音声の振幅スペクトル系列から位相スペクトル系列を深層学習で直接推定するときarctan(atan2)を経由するのも悪くないねという話

はじめに

音声の位相スペクトル系列を振幅スペクトル系列から復元する手法はいくつも提案されており,再現実装を試みた記事をこれまで書いてきた.

特に位相スペクトルの差分特徴を深層学習(DNN)により推定し,そこから位相スペクトルを復元するMasuyama氏らの手法は注目に値する. その手法のモチベーションのひとつには,位相スペクトルをDNNで直接推定することの難しさがあった.例えば,波形のわずかな時間シフトに対して位相スペクトルは値が大きく変動するという特徴である.振幅スペクトルの値は時間シフトでほとんど変わらないことから,位相スペクトル推定の困難さが増すこともよく了解できる.あくまでも時間方向・周波数方向の差分特徴量の推定にDNNを用い,位相スペクトル自体は後段のアルゴリズムに任せるというMasuyama氏らの方針は合理的でもあった.

さて最近の研究を少し追ってみると,音声合成や音声強調の文脈で,位相スペクトルをDNNにより直接推定する手法を複数確認している(DNNの入力は振幅スペクトル):

なかでも位相スペクトルを推定するために,仮想的な実部・虚部をDNNが出力し,arctan (atan2) 経由で推定する方法(Chin-Hui Leeらのグループ,Zhen-Hua Lingらのグループ)に興味を惹かれた.位相スペクトルを直接DNNの出力から得る手法だと復元音声の音質があまり良くないという「先入観」が筆者にあったのだが,先の論文を見るにそのようなDNNでも高品質な音声が復元できたらしい.つまり位相スペクトルの推定精度が十分に高くなった結果が報告されている.

そこで本記事では,DNNに基づく位相スペクトル推定において,arctan (atan2) を経由させた場合にどれほどの音質が達成できるのかを具体的に検証し,その結果を報告する. 特定の先行手法を再現実装するわけではない.

実装

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

github.com

図1にアーキテクチャ全体を示す.図2から図4には構成要素を示す.便宜上,入力側からPreNet,MiddleNet,PostNetと名前をつけた.

図1: アーキテクチャ全体

図2: PreNetのアーキテクチャ

図3: MiddleNetのアーキテクチャ

図4: PostNetのアーキテクチャ

ネットワークアーキテクチャはconv1dとresidual connectionを基本にしている.活性化関数にはGELUを用いた.あまり凝ったアーキテクチャにするのは本意ではないが,後段のresidual block(PostNet)にGRUを差し込むのは実際に有効だった.GRUの数が多い分,訓練に時間が取られるのはネックだが.

損失関数にはvon-Mises DNNで使われたcosine distance lossおよび瞬時周波数・群遅延の正則化項を加えたものを採用した.正則化項に重み係数は導入しておらず,単純に足し合わせただけである.

評価実験

JSUTコーパスのbasic5000を訓練データに,onomatopee300をテストデータに用いた.客観評価指標にESTOI, (wideband) PESQ,LSC (log-spectral convergence)を採用した.

実験条件

訓練の基本的な設定は以下の通りである.オプティマイザはRAdamであり, decoupled_weight_decay を Trueとしてweight_decay (重み減衰の強さ)を設定した.

項目 設定
ミニバッチサイズ 64
エポック数 1000
オプティマイザ RAdam
学習率 0.001
勾配クリッピングしきい値 10.0
重み減衰の強さ 0.1
学習率スケジューラ linear warmup つき
cosine annlealing (半周期)
warm up のエポック数 5
warm up 開始時の学習率 0.000001
annealing の終端での学習率 0.00001

今回はbasic5000の各音声クリップを1.0秒ごとに事前に分割した.分割により生じた1.0秒未満の端数は訓練に利用しない.最終的に24085個の音声クリップに分割された.その他の細かい設定は上記リポジトリのconfig.pyを参考にしてほしい.ちなみにネットワークのパラメータ数は5.7Mであった(5,715,842).

冒頭に示した記事より "RPU","wRPU"(重み付きRPU),"PGHI"のスコアを比較手法として引用する.

実験結果

図5:客観評価指標の箱ひげ図

図5に各評価指標の箱ひげ図を示す.arctanを経由する手法は"ATAN"で示されている.まずATANはいずれの指標においてもRPUのスコアを大きく上回っており,一定以上の音質を達成できたことが示された.ESTOIについて,wRPUに比べて大きく下がっているものの,PGHI程度のスコアは得られている.続いてPESQではwRPUに匹敵するスコアを達成できた(すごい).最後にLSCでもwRPUを上回る収束を示しているが,PGHIには及ばなかった.以上より,少なくとも客観評価指標のうえでは高品質な音声を復元できたと言えるだろう.

考察

学習用・評価用データの準備のために,STFTの複素スペクトルから位相スペクトルを抽出する時点でatan2を使っている(numpyのangleも最終的にはatan2).そこでニューラルネットによる推定も同じatan2で揃えるのはある意味で自然とも言える.さてatan2はどの象限でも数値的に安定して偏角を計算できる点で優れている.ナイーブに割り算してarctanを計算するのではうまくいかない.実際ちょっと試したところ,損失関数の値が安定して減らなかった.そのほか,位相のラッピングにより戻り値の範囲が(-π, π]に限定される点にも注目したい.値の取りうる範囲が重要というならば,tanhやsigmoidを適当にスケーリングすればatan2を代用できる気もするが,少し試してみた感じではこちらもうまくいかなかった.この点が最終的な推定精度に与える影響はさらに精査する必要があるだろう.

参考まで,推定された位相スペクトルから復元した音声を示す.この音声に関する各手法の評価スコアも付記する.

手法 評価用音声 スコア
自然音声
N/A
RPU
ESTOI=0.981
PESQ=3.534
LSC=-13.50 [dB]
wRPU
ESTOI=0.998
PESQ=4.268
LSC=-18.22 [dB]
PGHI
ESTOI=0.992
PESQ=4.120
LSC=-23.84 [dB]
ATAN
ESTOI=0.989
PESQ=4.330
LSC=-19.27 [dB]

ATANでは位相スペクトルの推定誤差に伴った「ノイズ」が復元音声に混入していることが分かる.このノイズはフレームシフトの時間間隔すなわち0.008秒の間隔で発生していることが確認できている.本記事のATANは絶対的な位相(波形の絶対位置)の推定精度が低いのだが,群遅延つまり位相の周波数ビン間の関係は推定精度が高いようである.位相ズレに伴うノイズが入る分,音声の明瞭度を評価するESTOIは低く留まった.その一方で群遅延の推定精度向上によりフレーム内の波形包絡がきれいに出る.つまり音色の再現性が高いことを意味しており,裏を返せばスペクトルの歪みは小さい.それゆえPESQが高くなったと説明できる.ATANは訓練時に瞬時周波数と群遅延に関する正則化項を損失関数に加えただけで,群遅延自体の分布をexplicitにモデル化していないのだが,このような結果が得られたのは興味深い.

最近の先行研究たちが絶対的な位相の推定精度の問題を明示的に取り上げた様子は見られなかった.あくまでも最終的な復元音声のスコアに興味があるようだ.手法のデモ音声を聞いてみると先述のノイズが入った音声はなかったので(例えばここ URL),彼らの用いたネットワークアーキテクチャや損失関数が暗黙的に絶対的位相の推定精度を向上させていたのだろう.位相スペクトルをうまく推定しつつ高品質な音声が復元できているならば,わざわざ位相推定まわりのマニアックな面倒ごとを掘り下げる必要もないというわけである.

絶対的な位相の推定精度向上に向けて,現状のATANのアーキテクチャをベースにしたとき,どのような工夫を入れればよいのかはno ideaである.本記事の実験では音声サンプルを1秒に区切ってミニバッチを構成したが,これを2秒や3秒などもう少し長くしたうえでdilated convやattentionの仕組みを入れて,時間的により広い範囲のスペクトルを効果的に拾いながら特徴抽出する仕組みが必要なのかもしれない.根本的にアーキテクチャを変えるにせよ,先行研究で採用されたアーキテクチャの構成要素や損失関数の試行錯誤は避けられない.Zhen-Hua Lingらのグループが使っているanti-wrapping lossの導入は興味深く,先行研究と条件を揃えるという意味でも検討の価値はある.

JSUTコーパスのbasic5000では訓練データ量として少ないという可能性ももちろん考えられるので,もう少し規模の大きいデータセットを使った実験と検証もまた必要だろう.特に訓練データに多数の話者を含む「不特定話者モデル」による検証は避けられない.またデータ量不足を補うためのデータ拡張も本実験ではまったく施しておらず,その効果は未検証である.少なくともランダムにフレーム開始位置をずらしてSTFTする形式のデータ拡張は適用してもよいだろう.

おわりに

位相スペクトルを推定するときにはarctanatan2)を経由するとうまくいく可能性が高い,という実験結果が得られた.本記事で採用したアーキテクチャは絶対的な位相の推定にまだ課題を残しており,研究の余地は依然として多く残されているともいえる.

Ablation studyを本来しっかりやらねばならないが,本記事は論文ではないのでかなりサボってしまった.特にATANとアーキテクチャ(PreNet, MiddleNet, PostNet)を揃えたうえで,最終層でatan2を使わずにDNNの出力を推定結果として用いる手法(いわゆるvon-Mises DNN式)でどれくらい推定がうまく行くのかはきちんと確かめておきたい.これが次の位相復元シリーズのネタになるだろう.

位相スペクトルの有理関数近似に基づく位相復元手法(のプロトタイプ)をPythonで実装した

はじめに

お待ちかね(?)の位相復元シリーズである.

手法の説明

記法を簡単にするため,ある特定の音声フレームに固定して考える.このフレームに関して, 位相スペクトル \( \phi(\omega) \) が有理関数で近似できると仮定する.


\begin{align*}
\phi(\omega) \approx \arctan \left( \frac{a_0 + a_1 \omega + a_2 \omega^2 + \dots + a_n\omega^n}{b_0 + b_1 \omega + b_2 \omega^2 + \ldots + b_n\omega^n}\right)
\end{align*}

ここで \( \omega \) は角周波数である.有理関数に現れる分子・分母多項式の最大次数は \( n \) としておく. 本手法では,それら多項式の係数 \( a_0, a_1, \ldots, a_n, b_0, b_1, \ldots, b_n \) をニューラルネットで推定する.

位相スペクトル自体を直接ニューラルネットで推定する(回帰する)手法はこれまで多く提案されてきた.そのニューラルネットの入力は(対数)振幅スペクトルであり,出力は位相スペクトルの値であった.本記事の手法も入力は対数振幅スペクトルで共通しているが,位相スペクトルが有理関数の係数を介して間接的に推定される点が異なる.

位相スペクトルは非常に複雑なパターンを持っており,それをニューラルネットによって対数振幅スペクトルから直接推定する場合には強い非線形性が要求される.結果的にパラメタ数(層数・チャネル数)の大幅な増加を招き,訓練が難しくなりがちである.本手法のニューラルネットはあくまで係数の推定に留まっている.実用上,有理関数の多項式の次数を増やしすぎることなく,それなりの精度で位相スペクトルの推定を達成できる(と期待される).

有理関数近似はパデ近似の趣もあるが,微分係数の一致を取っているわけではないので,パデ近似ではない.そもそも真の位相スペクトルが未知なので,微分係数も未知であり(たとえ連続関数と仮定したとしても),それらの一致を測れない.

関連手法

Zhen-Hua Ling らは類似の位相推定手法を提案している (Link).彼らの手法では単位円上の1点を表す複素数の実部・虚部をDNNで推定し,atan2により位相を得る.ネットワークが実部・虚部を直接出力しているのであって,その点は本記事の手法と異なる.また彼らが同論文の中で提案している損失関数があるが,予備実験として本記事の手法にその損失関数を適用したところ,収束が芳しくなく,復元音声の音質は低く留まった.

プロトタイプ実装

スクリプトは以下のURLに置いた.

Demonstration script for phase recovery via rational function approximation. · GitHub

まず位相推定にあたり,arctanatan2 (Link) により実装する.

発話の全フレームに渡って係数を一斉に推定する。つまり,あるフレームについて注目したとき,ミニバッチあたり「(周波数ビン数)×(多項式の次数)」だけの係数を一度に推定する.なので,最終層のチャネル数は必然的に増える.

ネットワークはConv1dを2層重ねて作り,活性化関数にはReLUを選んだ.有理関数を周波数軸上の各点(毎回決まった点)において何度も評価するために,角周波数のべき乗をあらかじめ計算しておくなどの工夫を取り入れている.また複数フレームに渡って並列に有理関数を評価する工夫も入っている.

損失関数には復元した複素スペクトログラムと元のスペクトログラムとの最小二乗誤差を採用した.振幅スペクトルで実部・虚部を重みづけて二乗誤差を取っているとみなせる.

おわりに

有理関数近似ニューラルネットが訓練データ量を増やした位相復元タスクでどこまで動くのか,検証するのは今後の課題とする.層の数を増やせば復元性能の向上が見込めるのは(ある意味)当然なので,そちらの検討も進めたい.

【Emacs】bsにおけるバッファリストやバッファ切り替え対象を同じタブに限定したいとき

前回記事の関連. tabspaces.elから一部関数を拝借したので,ライセンスを付記する.

;; https://github.com/mclear-tools/tabspaces/blob/main/tabspaces.el
;; Copyright 2013-2015 Justin Talbott
;; Copyright (C) 2022 Colin McLear
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
(defun bs-config--same-tab-buffer-list (&optional frame tabnum)
  (let ((list
         (if tabnum
             (let ((tab (nth tabnum (frame-parameter frame 'tabs))))
               (if (eq 'current-tab (car tab))
                   (frame-parameter frame 'buffer-list)
                 (or
                  (cdr (assq 'wc-bl tab))
                  (mapcar 'get-buffer
                          (car (cdr (assq #'bs-config--same-tab-buffer-list
                                          (assq 'ws tab))))))))
           (frame-parameter frame 'buffer-list))))
    (seq-filter #'buffer-live-p list)))
(defun bs-config--same-tab (buffer)
  (let ((current-buffer-list
         (bs-config--same-tab-buffer-list
          nil (tab-bar--current-tab-index))))
    (with-current-buffer buffer
      (not (member buffer current-buffer-list)))))

(add-to-list 'bs-configurations
             '("same-tab" nil nil nil bs-config--same-tab nil))

設定は例えば:

(setq bs-default-configuration "same-tab")
(setq bs-cycle-configuration-name "same-tab")

【Emacs】bsにおけるバッファリストやバッファ切り替え対象を同じmajor mode に限定したいとき

bs-configurations という変数にフィルタリング用の関数を追加してやれば良いわけである.

ここでは"same-mode"という名前をつけてみた.このcodeはace-jump-bufferから抽出した(実際のコードはマクロ).GPL ライセンスの旨を併記しておく.

;; Copyright 2013-2015 Justin Talbott
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
(defun bs-config--same-mode (buffer)
  (let ((current-mode major-mode))
    (with-current-buffer buffer
      (not (eq major-mode current-mode)))))

(add-to-list 'bs-configurations
             '("same-mode" nil nil nil bs-config--same-mode nil))

そして 例えばこんな感じに設定する.

(setq bs-default-configuration "same-mode")
(setq bs-cycle-configuration-name "same-mode")

【Emacs】bm.elの設定例

こんな感じ。

ちなみに、mapc ... で始まる箇所は、バッファ(たち)をdesktop-modeで復元後にbmを復元するための設定である。それゆえ、desktop-modeを使っていないひとには不要な設定である。復元した全バッファに対して処理を回しているので、そもそもbmを置いていないバッファに対しては不要な処理が回っており、非効率であるが、実質的に処理時間は無視できる。

bm-repository に登録されている全ファイルを開く、という設定はお好みで。確かにbm.elは開いていないバッファ(ファイル)に対しては一覧機能が働かない(bm-show-all)ので、それを改善する一つの方策ではある。bm.elの作者にとって何らかの理由があったうえで、bm-show-allの対象がopen済のバッファたちに限定されているのであろう。ちなみにリポジトリでもissueとして挙げられている。

(setq bm-restore-repository-on-load t)
(add-hook 'emacs-startup-hook
          #'(lambda ()
              (require 'bm)
              (setq bm-cycle-all-buffers t)
              (setq-default bm-buffer-persistence t)

              ;; bm-repository に登録されている全ファイルを開く
              ;; http://emacs.rubikitch.com/bm-repository-open/
              ;; (defun bm-find-files-in-repository ()
              ;;   (interactive)
              ;;   (cl-loop for (key . _) in bm-repository
              ;;            when (file-exists-p key)
              ;;            do (find-file-noselect key)))
              ;; (bm-find-files-in-repository)

              ;; ファイルに関連する全バッファを対象にrestore
              (mapc #'(lambda (buf)
                        (with-current-buffer buf
                          (bm-buffer-restore)))
                    (cl-remove-if-not 'buffer-file-name (buffer-list)))
              (add-hook 'find-file-hooks #'bm-buffer-restore)
              (add-hook 'after-save-hook 'bm-buffer-save)
              (add-hook 'after-revert-hook #'bm-buffer-restore)
              (add-hook 'kill-buffer-hook #'bm-buffer-save)
              (add-hook 'kill-emacs-hook #'(lambda ()
                                             (unless noninteractive
                                               (bm-buffer-save-all)
                                               (bm-repository-save))))))

【Emacs】centaur-tabs-mode で黒系のアイコンがdark系カラーテーマのときに見えづらくなる(nerd-icon)

centaur-tabs-mode で黒系のアイコンの例は ".emacs" の nf-oct-gear だが、これはdark系カラーテーマ(例えばdoom-dracula)のときには見えづらい。 ほか、term-modeの nf-dev-terminal などもそうであり、結局これらにはfaceが付与されていないのである (nerd-icons.el)。

そこで centaur-tabs-icon を再定義した。

(defun centaur-tabs-icon (tab face selected)
  "Generate icon for TAB using FACE's background.
If icon gray out option enabled, gray out icon if not SELECTED."
  (if centaur-tabs-icon-type
      (with-current-buffer (car tab)
        (let* ((icon
                (or (ignore-errors
                      (centaur-tabs--icon-for-file
                       (file-name-nondirectory (buffer-file-name))
                       :v-adjust centaur-tabs-icon-v-adjust
                       :height centaur-tabs-icon-scale-factor))
                    (ignore-errors
                      (centaur-tabs--icon-for-mode
                       major-mode
                       :v-adjust centaur-tabs-icon-v-adjust
                       :height centaur-tabs-icon-scale-factor))))
               (background (face-background face nil 'default))
               (inactive (cond ((and (not selected)
                                     (eq centaur-tabs-gray-out-icons 'buffer))
                                (face-foreground 'mode-line-inactive nil 'default))
                               ((or centaur-tabs-plain-icons
                                    (ignore-errors ;; for .emacs
                                      (eq (string-match "^\\." (file-name-nondirectory (buffer-file-name))) 0))
                                    (derived-mode-p 'term-mode))
                                (face-foreground 'centaur-tabs-selected nil 'default))
                               (t 'unspecified)))
               (underline (and (eq (if (display-graphic-p) centaur-tabs-set-bar) 'under)
                               (face-attribute face :underline)))
               (overline (and (eq (if (display-graphic-p) centaur-tabs-set-bar) 'over)
                              (face-attribute face :overline))))
          (if (stringp icon)
              (progn
                (propertize icon 'face `(:inherit ,(get-text-property 0 'face icon)
                                                  :foreground ,inactive
                                                  :background ,background
                                                  :underline ,underline
                                                  :overline ,overline)))
            "")))
    ""))

【Emacs】tab-bar-mode のタブ削除・新規タブのボタンが小さいので大きくした / How to make the tab deletion and new tab buttons in tab-bar-mode enlarged

こうする.define-iconで調整する.修正の本質は

:height (1.0 . em)

を追加することにあり,この1.0を変えれば大きさも変化する.タブ履歴のほうは heightを0.9にしてみた.

ただしこのときのEmacsのバージョンは29.4である.

(with-eval-after-load 'tab-bar

  (setq tab-bar-new-button-show t)  ;; nil: 新規タブボタンは非表示
  (setq tab-bar-close-button-show t) ;; nil:タブ削除ボタンは非表示

  (setq tab-bar-new-tab-to 'right) ;; 新規タブは一番右

  ;; 各ボタンの有無はお好みで
  (setq tab-bar-format '(tab-bar-format-history
                         tab-bar-format-tabs
                         tab-bar-separator tab-bar-format-add-tab))

  (setq tab-bar-history-limit 100) ;; ヒストリの最大履歴

  (require 'icons) ;; define-icon をロードするため
  
  ;; 新規タブマークが小さすぎるので大きくする
  (define-icon tab-bar-new nil
    `((image "tabs/new.xpm"
             :height (1.0 . em)
             :margin ,tab-bar-button-margin
             :ascent center)
      (text " + "))
    "Icon for creating a new tab."
    :version "29.1"
    :help-echo "New tab")

  ;; タブ削除マークが小さすぎるので大きくする
  (define-icon tab-bar-close nil
    `((image "tabs/close.xpm"
             :height (1.0 . em)
             :margin ,tab-bar-button-margin
             :ascent center)
      (text " x"))
    "Icon for closing the clicked tab."
    :version "29.1"
    :help-echo "Click to close tab")

  ;; タブの履歴マークも小さすぎるのでついでに大きくする
  (define-icon tab-bar-back nil
    `((image "tabs/left-arrow.xpm"
             :height (0.9 . em)
             :margin ,tab-bar-button-margin
             :ascent center)
      (text " < "))
    "Icon for going back in tab history."
    :version "29.1")
  (define-icon tab-bar-forward nil
    `((image "tabs/right-arrow.xpm"
             :height (0.9 . em)
             :margin ,tab-bar-button-margin
             :ascent center)
      (text " > "))
    "Icon for going forward in tab history."
    :version "29.1")

  (setq tab-bar-history-limit 100) ;; ヒストリの最大履歴

  (tab-bar-mode 1)
  (tab-bar-history-mode 1) ;; 各タブについて,ウィンドウ構成のヒストリを記憶する
  )

Emacs開発者のブランチにはボタン拡大のコミットがされており,Emacs 30 系では反映されていると思われる.

git.savannah.gnu.org

tab-bar-mode についてはこちらの記事も参考に.

tam5917.hatenablog.com