やってみた!

やってみた!

試したことを中心に、書評や興味のあること、思ったこととか

Jetson NanoでTacotron2(音声合成)を動かす

 Jetson nanoでTacotron2を動かしてみます。Tacotron2の詳細は記事をご覧ください。

 なお本記事は2019年11月末時点のものです。ツールのバージョンアップ等により、この手順で動かなくなることもありうるので、あらかじめご容赦ください。

github.com

先に結果を報告します。

  • Jetson NanoでTacotron2+WaveGlowが動きました。
  • 実行にかかる時間は約3分ちょいでした(そのうちモデル読み込み時間が約2分)。nvpmodeはmax(10W 4コア)、クロックも最大設定です(gpu 921.6MHz)。
  • NVIDIA実装の英語のままです。
  • メモリはぎりぎりで、Tacotron2、WaveGlowの片方でほぼ一杯。Nanoは4GBのメモリがありますが、常時約1.6GBはシステムが占有しており、残りだと精一杯のようです。今回、Docker Containerで動かしましたが、ホストOS側でSwap設定が必須です。
  • インターネットブラウザ等、他のアプリは全て落として動かさないと、メモリSwapでえらく時間がかかります(下手すると数10分以上)。
  • 高速化の余地はだいぶありそうですが、専門的知識、経験が無いと難しいかも。

 興味がある方は以下の手順で試してみてください。環境構築するのに1,2時間かかります。docker containerを使うのでSDカードは容量に余裕があるもの(64GB以上)を推奨します。だいたいcontainerで5GB~6GBくらい消費します。

 あと、Jetson NanoはHDMI経由で音を出します。HDMIに接続されているディスプレイにスピーカがついているか事前に確認が必要です。

 なお、Jetsonを持ってないけど試してみたい方はこちら。Google Colaboratoryで試しています。
akifukka.hatenablog.com

 1.JETPACK4.2.2の準備

 本記事ではDocker Containerを使います。Containerを使うにはJETPACK4.2.2以降が必要です。JETPACK4.2.2のセットアップは次を参考にしてください。

 メモリSwapもJETCARDをインストールすると自動的に設定してくれます。

akifukka.hatenablog.com

2.DeepStream-l4t Containerの作成、起動

 Tacotron2をContainerで動かします。まずはl4t-base Container(r32.2.1)を作成、起動します。

sudo xhost +si:localuser:root
sudo docker run --runtime nvidia --network host -it -e DISPLAY=$DISPLAY -v /tmp/.X11-unix/:/tmp/.X11-unix --device /dev/snd:/dev/snd nvcr.io/nvidia/l4t-base:r32.2.1

4.環境構築

 Containerの端末で次を実行します。

  • update
    apt-get update
  • PIP3
    wget https://bootstrap.pypa.io/get-pip.py
    python3 get-pip.py
  • git
    apt-get install git
  • apex pip3でlibblas.so.3 errorが出るのを回避
    apt-get install libatlas3-base
  • コンパイラ
    apt-get install -y --no-install-recommends make g++
  • python3の開発ツール
    apt-get install python3.6-dev
  • pytorch1.0 少し時間がかかります。
    wget https://nvidia.box.com/shared/static/2ls48wc6h0kp1e58fjk21zast96lpt70.whl -O torch-1.0.0a0+bb15580-cp36-cp36m-linux_aarch64.whl

pip3 install numpy torch-1.0.0a0+bb15580-cp36-cp36m-linux_aarch64.whl

  • scipy 
    apt-get install python3-scipy

 なお、python2,3自体は最初からcontainerにインストールされています。

5.Tacotron2関係のインストール

(1)tacotron2

git clone https://github.com/NVIDIA/tacotron2.git

(2)submodule(waveglowなど)導入
waveglowを最新版にしないと後でdenoiserが無いというエラーが出るので、--remote --mergeで最新版に更新しています。

 以降はtacotron2フォルダで作業します。

cd tacotron2
git submodule init
git submodule update --remote --merge

(3)Apexインストール

git clone https://github.com/NVIDIA/apex
cd apex
pip3 install -v --no-cache-dir ./

(4)matplotlib

apt-get install python3-matplotlib=2.1.1-2ubuntu3

 地域を聞かれるのでAsia-Tokyoを選択します。

(5)tensorflowインストール

apt-get install libhdf5-serial-dev hdf5-tools libhdf5-dev zlib1g-dev zip libjpeg8-dev python3-h5py
pip3 install -U future==0.17.1 mock==3.0.5 keras_preprocessing==1.0.5 keras_applications==1.0.6 enum34 futures testresources setuptools protobuf
pip3 install --extra-index-url https://developer.download.nvidia.com/compute/redist/jp/v42 tensorflow-gpu==1.13.1+nv19.3
pip3 install tensorboardX==1.1tensorboardX==1.1 unidecode==1.0.22 pillow inflect==0.2.5 

(6)librosaインストール

 librosaのインストールは次のホームページに助けられました。Thanks!

https://learninone209186366.wordpress.com/2019/07/24/how-to-install-the-librosa-library-in-jetson-nano-or-aarch64-module/

ア. llvmliteのインストール

apt-get install llvm-7
cd /usr/bin
ln -s llvm-config-7 llvm-config
cd /tacotron2
pip3 install llvmlite
apt-get install libblas-dev liblapack-dev libatlas-base-dev gfortran

イ.cython

pip3 install cython

ウ.pip3アップグレード

pip3 install --upgrade pip

エ.librosaのインストール 時間がかかります

pip3 install librosa==0.6.0

6.学習済みモデルのダウンロード

(1)WaveGlow

wget https://api.ngc.nvidia.com/v2/models/nvidia/waveglow_ljs_256channels/versions/3/files/waveglow_256channels_ljs_v3.pt

(2)Tacotron2

wget --load-cookies /tmp/cookies.txt "https://docs.google.com/uc?id=1c5ZTuT7J08wLUoVZ2KkUs_VdZuJ86ZqA&export=download&confirm=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate 'https://docs.google.com/uc?id=1c5ZTuT7J08wLUoVZ2KkUs_VdZuJ86ZqA&export=download&id=FILEID' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=FILEID" -O tacotron2_statedict.pt && rm -rf /tmp/cookies.tx

7.Pythonからサウンド出力するためのライブラリ

apt-get install libasound-dev
apt-get install portaudio19-dev python-all-dev
pip3 install pyaudio

 8.動かしてみる

①inference.pyとして以下をtactron2フォルダに置く。viエディタで 'i’ でインサートモードにしてコピペすれば複製できます。

②他のソフト、Chrome等を全て終了させます。終了させないとメモリが足りなくなって時間がものすごくかかったりします。

③実行します。

python3 inference.py

④warningがたくさん出ますが気にしないで、高速化設定のJetson Nanoで3分半ほど待つとmel spectrogramの画面が表示されます。画面をx印をクリックして終了させると音声が出ます。日本語風はあまり聞き取れませんが・・・。

・waveglow is really awesome!

・こんにちわ、今日もいい天気ですね!

 exitを入力するとcontainerを終了させることができます。

import matplotlib
import matplotlib.pylab as plt
import sys
sys.path.append('waveglow/')
import numpy as np
import torch
import pyaudio
import gc
import time

from hparams import create_hparams
from model import Tacotron2
from layers import TacotronSTFT, STFT
from audio_processing import griffin_lim
from train import load_model
from text import text_to_sequence
from denoiser import Denoiser

def plot_data(data, figsize=(16, 4)):
    fig, axes = plt.subplots(1, len(data), figsize=figsize)
    for i in range(len(data)):
        axes[i].imshow(data[i], aspect='auto', origin='bottom',
                       interpolation='none')

torch.cuda.synchronize()
time0 = time.time()

hparams = create_hparams()
hparams.sampling_rate = 22050

checkpoint_path = "tacotron2_statedict.pt"
model = load_model(hparams)
model.load_state_dict(torch.load(checkpoint_path)['state_dict'])
_ = model.cuda().eval().half()

torch.cuda.synchronize()
time1 = time.time()
print('Tacotron2 load',time1 - time0)

text = "Waveglow is really awesome!"
sequence = np.array(text_to_sequence(text, ['english_cleaners']))[None, :]
sequence = torch.autograd.Variable(
    torch.from_numpy(sequence)).cuda().long()

mel_outputs, mel_outputs_postnet, _, alignments = model.inference(sequence)

text2 = "kon nichiwa!,kyou moiitenkidesue ne!"
sequence2 = np.array(text_to_sequence(text2, ['english_cleaners']))[None, :]
sequence2 = torch.autograd.Variable(
    torch.from_numpy(sequence2)).cuda().long()

mel_outputs2, _, _, _ = model.inference(sequence2)

torch.cuda.synchronize()
time2 = time.time()
print('Tacotron2 infer ',time2 - time1)

del model
gc.collect()

torch.cuda.synchronize()
time3 = time.time()

waveglow_path = 'waveglow_256channels_ljs_v3.pt'
waveglow = torch.load(waveglow_path)['model']
waveglow.cuda().eval().half()
for k in waveglow.convinv:
    k.float()
denoiser = Denoiser(waveglow)

torch.cuda.synchronize()
time4 = time.time()
print('waveglow init ',time4 - time3)

with torch.no_grad():
    audio = waveglow.infer(mel_outputs_postnet, sigma=0.666)
    audio2 = waveglow.infer(mel_outputs2, sigma=0.666)

torch.cuda.synchronize()
time5 = time.time()
print('waveglow infer ',time5 - time4)

np.save('audio',audio[0].data.cpu().numpy())
np.save('audio2',audio2[0].data.cpu().numpy())

list_audio = []
for i in audio[0].data.cpu().numpy():
    list_audio.append(i)
    list_audio.append(i)

audio1_2 = np.array(list_audio)

list_audio2 = []
for i in audio2[0].data.cpu().numpy():
    list_audio2.append(i)
    list_audio2.append(i)

audio2_2 = np.array(list_audio2)

p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paFloat32,
                channels = 1,
                rate = 44100,
                output = True,
                frames_per_buffer = 1024)

plot_data((mel_outputs.float().data.cpu().numpy()[0],
           mel_outputs_postnet.float().data.cpu().numpy()[0],
           alignments.float().data.cpu().numpy()[0].T))
plt.show()

print("play")
stream.write(audio1_2.astype(np.float32).tostring())
stream.write(audio2_2.astype(np.float32).tostring())

stream.close()

f:id:akifukka:20191130204515p:plain

9.containerの再実行

・CONTAINER_IDを取得します。
 sudo docker ps -a

・実行させます。-iを忘れずに。
 sudo docker start -i CONTAINER_ID

・なお、CONTAINER_IDをわかりやすい名前に変えることもできます。
 sudo docker rename CONTAINER_ID 変更後のCONTAINER_ID

10.まとめ

  Jetson NanoでTacotron2音声合成を試してみました。かろうじて動きましたが、リアルタイムとは行きませんでした。

一応、2行分で、time関数で処理時間を計測した結果は次の通り。

 tacotron2 6.5s、waveglow 20.6s

 実際はモデル読み込みに2分38秒、その他ソフト初期化等で30秒弱といったところです。高速化チューニングで処理時間(モデル読み込み、ソフト初期化を除く)を数分の一程度に短縮できる可能性はあると思いますが、それでもまだまだですね。

 といっても、Jetsonザビエルあたりだとリアルタイムに動くかも知れません。ハードウェアの高速化を待った方がよさそうですね。

 日本語化も課題ですが、ちょっと簡単にはいかなさそうなので、次どうするかは未定です。

Jetson Nano カテゴリーの記事一覧へ

音声合成 カテゴリーの記事一覧へ

Tacotron2を調べてみた3

  前回はDecoderの概要部分でしたので、次は中身を見ていきたいと思います。

 2.6 decode

 クラスDecoderのinference中の次の文の中身を見てみます。

 mel_output, gate_output, alignment = self.decode(decoder_input)

 この文は、クラスDecoderのメソッドdecodeを呼び出しています。メソッドdecodeを次に示します。decoder_inputは前回推定したmel spectrogramをprenetに通したものでで大きさ(1,256)のテンソルです。

    def decode(self, decoder_input):
        """ Decoder step using stored states, attention and memory
        PARAMS
        ------
        decoder_input: previous mel output

        RETURNS
        -------
        mel_output:
        gate_output: gate output energies
        attention_weights:
        """
        cell_input = torch.cat((decoder_input, self.attention_context), -1)
        self.attention_hidden, self.attention_cell = self.attention_rnn(
            cell_input, (self.attention_hidden, self.attention_cell))
        self.attention_hidden = F.dropout(
            self.attention_hidden, self.p_attention_dropout, self.training)

        attention_weights_cat = torch.cat(
            (self.attention_weights.unsqueeze(1),
             self.attention_weights_cum.unsqueeze(1)), dim=1)
        self.attention_context, self.attention_weights = self.attention_layer(
            self.attention_hidden, self.memory, self.processed_memory,
            attention_weights_cat, self.mask)

        self.attention_weights_cum += self.attention_weights
        decoder_input = torch.cat(
            (self.attention_hidden, self.attention_context), -1)
        self.decoder_hidden, self.decoder_cell = self.decoder_rnn(
            decoder_input, (self.decoder_hidden, self.decoder_cell))
        self.decoder_hidden = F.dropout(
            self.decoder_hidden, self.p_decoder_dropout, self.training)

        decoder_hidden_attention_context = torch.cat(
            (self.decoder_hidden, self.attention_context), dim=1)
        decoder_output = self.linear_projection(
            decoder_hidden_attention_context)

        gate_prediction = self.gate_layer(decoder_hidden_attention_context)
        return decoder_output, gate_prediction, self.attention_weights

ソースコードだと全体像がわかりずらいので図にしました。

f:id:akifukka:20191127222347j:plain


decodeの入力
・decoder_input    前回のmel spectrogramをprenetに通したもの
・encoder_output  encoderの出力
decoderの出力
・attention_weights 位置検出結果、出力は状態確認(表示)で使用しているのみ
・gate_prediction  終了判断に使用
・decoder_output  mel spectrogram

 構造を見てわかるように、attention_hidden、processed_memory、attention_weights_catの3信号をQueryとして、encoder出力をKey及びValueとしてAttentionを行っています。

 ここから先は推測まじりですが、

 終了を最後のgate_layerで判定できているので、attention_layerの出力attention_contextにどこまで進んだかを示す信号が含まれていると思われます。
 encoderの出力は文字単位なので、attention_contextも基本的には文字単位のデータなのですが、このデータの中に1文字の間に変化する音の情報も含まれていると思われます。この後の処理のどこかで、このデータから今の瞬間の音に関する情報を抽出してmel spectrogramを生成しているのでしょう。ただし、attentionの後ろに複雑なネットワーク構造は無いので、1文字中ではあまり複雑な変化に対応できないかも知れません。

2.7 Location Sensitive Attenation

 こちらもわかりやすよう図にしました。

f:id:akifukka:20191128211043j:plain

 get_alignment_energiesで現在が各文字の位置にある可能性を推測、softmaxで正規化しています。その後、現在の位置に相当する文字に相当する情報をattention_contextとしてエンコーダの出力から抽出します。

 2.6 decodeの項に戻り、さらにいくつか処理をして現在のmel_spectrogramと、終了を示すフラグgate_predictionを推測します。

2.8 Post Net

 Post Netは5層の1次元Convolution Network(畳み込みニューラルネットワーク)です。

mel_outputs_postnet = self.postnet(mel_outputs) mel_outputs_postnet mel_outputs_postnet = mel_outputs + mel_outputs_postnet

Postnetの仕様は次の通りです。

・入力 80
・フィルタ数 512 フィルタ長 5
・出力80

 mel_outputsの大きさは(1,80,n)、nは時間方向のspectrum数です。

 時間方向に畳み込みをしてmel spectrogramの時間方向のなめらかさ?改善のための補正量を推定し、Post Netを通る前のmel_outputsに加算して補正をかけます。

 個人的には補正前後で大きな違いは感じられませんでした。

 2.9 Tactron2.inferenceの出力

 クラスTactoron2のinferenceからparse_output経由で出力されます。

outputs = self.parse_output(
[mel_outputs, mel_outputs_postnet, gate_outputs, alignments])

return outputs

 ・mel_outputs、mel_outputs_postnet
 mel spectrogramです。テンソルのサイズは(1,80,n)でnは時間軸方向のデータ数です。

・gate_outputs
 終了フラグです。サイズは(1,n,1)です。その時々のgateの値を保管したものです。基本的に、音声合成では使いません。

・alignments
 位置あわせを示すテンソルです。サイズは(1,n,文字数)でnは時間軸方向のデータ数です。

3. 補足 Tacotron2のinference実行

 Tacotron2のプロジェクトフォルダにあるJupitorノートブックでは次の流れで呼び出しています。一応補足まで。

①from train import load_model
 load_modelをインポート

②model = load_model(hparams)
 load_model呼び出して、modelを生成。

③train.pyのload_model
 def load_model(hparams):
 model = Tacotron2(hparams).cuda()
 ・・・
 return model

 ここで、クラスTacotron2を生成してリターンしているので、②のmodelはTacotron2になる。

④inference呼び出し
 mel_outputs, mel_outputs_postnet, _, alignments = model.inference(sequence)
 model=Tacotron2なので、Tacotron2のメソッドinferenceを呼び出します。ここから今まで見てきた処理が始まります。

4.まとめ

 NVIDIA実装をもとにTacotron2の構造を見てみました。次回はJetson Nanoで動かしてみます。

音声合成 カテゴリーの記事一覧へ

Tacotron2を調べてみた2

 前回の続きです。誤記、わかりやすくするため、たびたび加筆、修正するかも知れませんが、ご容赦頂きたく。
akifukka.hatenablog.com

 2.3 Decoderの概要

 ここからはmodel.pyのクラスTacotron2のinferenceの次の文の中身になります。Decoderはちょっと入り組んでいるので、最初に概要を解説した後に構成要素を詳しく見ていくことにします。

 次の文はクラスDecoderのinferenceを呼び出します。

mel_outputs, gate_outputs, alignments = self.decoder.inference(
   encoder_outputs)

 クラスDecoderのinferenceを次に示します。

    def inference(self, memory):
        """ Decoder inference
        PARAMS
        ------
        memory: Encoder outputs

        RETURNS
        -------
        mel_outputs: mel outputs from the decoder
        gate_outputs: gate outputs from the decoder
        alignments: sequence of attention weights from the decoder
        """
        decoder_input = self.get_go_frame(memory)

        self.initialize_decoder_states(memory, mask=None)

        mel_outputs, gate_outputs, alignments = [], [], []
        while True:
            decoder_input = self.prenet(decoder_input)
            mel_output, gate_output, alignment = self.decode(decoder_input)

            mel_outputs += [mel_output.squeeze(1)]
            gate_outputs += [gate_output]
            alignments += [alignment]

            if torch.sigmoid(gate_output.data) > self.gate_threshold:
                break
            elif len(mel_outputs) == self.max_decoder_steps:
                print("Warning! Reached max decoder steps")
                break

            decoder_input = mel_output

        mel_outputs, gate_outputs, alignments = self.parse_decoder_outputs(
            mel_outputs, gate_outputs, alignments)

        return mel_outputs, gate_outputs, alignments

 論文のブロック図を再度掲載します。

f:id:akifukka:20191118220212j:plain
 (1)Decoderの初期化

decoder_input = self.get_go_frame(memory)
 decoder_inputは前回推定したmel spctrogramをいれるテンソルで、2layer Pre-Netに入力するものです。最初は全て0に初期化します。get_go_frameはクラスDecoder内で定義されているメソッドです。memoryのサイズは(1,文字数,512)、ここでのdecoder_inputのサイズは(1,80)です。

self.initialize_decoder_states(memory, mask=None)
 decoderの内部状態の初期化をします。また、memory(Encoder出力)を内部変数のself.memoryに保管して、これ以降関数呼び出しでmemoryを引数に設定する必要を無くしています。

(2)whileループ

 発声の時間軸を追って1タイムステップ(256/22050=11.6ms?)ずつ、mel spectrogramを推定していきます。

decoder_input = self.prenet(decoder_input)
 decoder_inputには前回推定したmel spectrogramが入っています。これを256要素、全結合2層ニューラルネットワークのprenetに通します。
 GoogleのTacotron2の論文によると、このprenetによる情報の制限はattntionの学習の本質にかかわるとのことです。
 Encoderから入力される信号は基本的に文字数と同じですが、12.5msずつ推定されるmel spectrogramの時間の長さと1対1の関係になりません。そのため何らかの方法で、ある文字の発声の中のどのあたりを推定しているのかを知る必要があります。前回のmel spectrogramの情報を使うことで、どこまで進んだかを知ることができます。
 prenetを通した後のdecoder_inputのサイズは(1,256)です。

mel_output, gate_output, alignment = self.decode(decoder_input)
mel spectrogram、gate_outpupt、を推定します。詳細は別途。
・mel spectrogram
 1ステップ分のmel spectrogramで、テンソルの大きさは(1,80)です。
・gate_output
 終了判定用信号で大きさは(1,1)です。
・alignment
 今どの文字部分の推定をしているかを示すテンソルで大きさは(1,文字数)です。

③1ステップ分の推定値をリストに追加
mel_outputs += [mel_output.squeeze(1)]
gate_outputs += [gate_output]
alignments += [alignment]

④終了判定
if torch.sigmoid(gate_output.data) > self.gate_threshold:
     break 
gate_outputの値をsigmoid関数を通した後の値で終了判定をしています。gate_thresholdは0.5です。

⑤mel_outputをdecoder_input に代入して、①に戻ります。

(3)出力準備

 次の関数内でテンソルのリストを結合し、軸を増やしてテンソルに変換します(toarch.stack関数を使用)。

mel_outputs, gate_outputs, alignments = self.parse_decoder_outputs(
       mel_outputs, gate_outputs, alignments)

 2.4 クラスPrenet

 prenetは前回のステップで推定したmel sptectrogram(これから推定するものに対して11.6ms前のmel sptectrogram)からLocation Sensitive Attentionに入力する特徴を抽出するものです。主に今どこの時間の処理をしているかをAttentionに伝える働きをしていると思われます。
 prenetは次の文で呼び出していますが、prenetはクラスDecodeのメソッドでは無く、独立したクラスPrenetのインスタンスです。

decoder_input = self.prenet(decoder_input)

 self.prenetはクラスDecoderの__init__(コンストラクタ)の次の文でクラスPrenetのインスタンスとして生成されています。

self.prenet = Prenet(
  hparams.n_mel_channels * hparams.n_frames_per_step,
  [hparams.prenet_dim, hparams.prenet_dim])

 パラメータは次の通りで、入力80、大きさ256の隠れ層を2層もった出力256のネットワークを生成します。
hparams.n_mel_channels = 80
hparams.n_frames_per_step = 1
hparams.prenet_dim = 256

 クラスPrenetを次に示します。

class Prenet(nn.Module):
    def __init__(self, in_dim, sizes):
        super(Prenet, self).__init__()
        in_sizes = [in_dim] + sizes[:-1]
        self.layers = nn.ModuleList(
            [LinearNorm(in_size, out_size, bias=False)
             for (in_size, out_size) in zip(in_sizes, sizes)])

    def forward(self, x):
        for linear in self.layers:
            x = F.dropout(F.relu(linear(x)), p=0.5, training=True)
        return x

 クラスPrenetの__init__でin_sizes=[in_dim]+size[:-1]のsize[:-1]で層の入力数を、sizesで出力数を設定しています。-1はsizeの後ろから数えて1つ目を意味し、in_sizesは最初はin_dim~最後から1層前の層の要素数のリストになります。LinearNormはTacotron2プロジェクトのlayers.pyで定義されており、nn.Linearを使った線形ニューラルネットワークです。

 さて、prenet(decoder_input)はメソッド(関数)として使っていますが、実体はどこでしょうか?。自分が実体を探すのに手間取ったので、ちょっとpythonの文法について触れておきたいと思います。

 メソッド名が省略されると、pythonでは__call__メソッドが呼ばれます。クラスPrenetには__call__の記載がありませんが、Prenetはpytorchのnn.Moduleを継承しているので、nn.Moduleの__call__が呼ばれます。

  pytorchのソースはGithubにあり、torch/nn/modules/module.pyにクラスModuleのの中身があります。

 GitHub - pytorch/pytorch: Tensors and Dynamic neural networks in Python with strong GPU acceleration

 クラスnn.Moduleの__call__はニューラルネットワークの推定処理forwardに加えて、あらかじめユ-ザがHookとして登録したメソッドを一緒に実行するよう作られています。Hookの登録方法等はpytorchのnnのドキュメントに書かれています。ご参考まで。

torch.nn — PyTorch master documentation

2.5 Attention(注意機構)の概要

  ここではイメージをつかむためAttentionの基本について簡単に紹介します。

 Attentionはある情報に着目(注意)して出力を作る仕組みのことです。Tactron2では、”1ステップ前は、どの文字のどこのmel spectrogramを作った”という情報に着目して、”じゃあ次のspectrumはこれ”といった感じで動きます。

f:id:akifukka:20191123182335j:plain

 機械翻訳を例にすると、

①"Ihave a pen."をencoderで分析します。言葉の場合、順番が重要なのでLSTM等を使って語句の順番も踏まえて分析します。

②Attentionを使ってdecodeします。Attentionの代表的な作動は次の通りです。

・入力はquery、key、value。keyとvalueはencoderの出力情報、queryはdecoderの直前の出力とすることが多い。

・queryにattention(注意)して、最も親和性の高い(関連する、一致する)keyを抽出。

・keyに相当するvalueを抽出して出力とする。

③例えば、"私は"が前回のdecoder出力だとすると、queryとして”私は"をattentionに入力、次に来るのが動詞、目的語どちらが良いかを計算(テンソルの掛け算)すると、より好ましい方(例えば目的語)の評価値が大きくなる。

④続いてvalueテンソルと掛け算すると、評価値の高いkeyである目的語に相当するvalue”ペンを”の評価値が高くなるので、次の出力として”ペンを”を選ぶ。

 実際はこんなに簡単にはならない(主語の次は目的語が良いという情報はどこに含ませる?等)ので、ニューラルネットワークを挿入して情報を補完する必要があります。

 Tactron2では、Attentionを音声合成がどこまで進んだかを検出する(alignment)のに使っています。前回のmel spectrogramをQueryとする他、内部状態も使って位置あわせをしています。前回のmel spectrogramだけでは文章中の同じようなspqctrum=発音と混同するなど、間違う可能性があるということなのでしょう(だからLocation Sensitive Attentionという名称なのかも知れません)。

 Tacotron2の論文からAttention部分について引用されている論文へのリンクを2つ貼り付けておきます。

 Tacotron2は音声合成ですが学習時は音声とうまくスペルの位置を合わせられないと、まともに学習できないだろうと想像されます。学習側のソースは読んでないので想像ですが・・・。Attentionの規模が大きいのにも納得です・・。

arxiv.org

まだつづきます

音声合成 カテゴリーの記事一覧へ

Tacotron2を調べてみた1

 以前Colaboratoryで試して、英語の音声合成ができることはわかったので、日本語を目標にまずはtacotron2の中身を解説してみます。Googleの論文とNVIDIA実装を中心に見ていきます。

過去にColaboratoryで試した時の記事
akifukka.hatenablog.com

 GoogleのTacotron2論文

arxiv.org

NVIDIAによるTacotron2実装

github.com

 1.概要

 Tacotron2はGoogleで開発されたTTS(Text To Speech)アルゴリズムです。テキストをmel spectrogramに変換、mel spectrogramを音声波形に変換するという大きく2段の処理でTTSを実現しています。本家はmel spectrogramを音声波形に変換する箇所はWavenetからの流用で、Tacotron2の本体はテキストからmel spectrogramへの変換部分と言えます。なお、NVIDIA実装では処理軽量化のため音声波形への変換部分をWaveGlowに変更しています。

 次に示すのは論文に掲載されていた全体構造の図(ちょっと追記しました)です。Tacotron2の処理は大きく次の4つに分けられます。
①Character Enbedding
②Encoder
③Decoder
・Location Sensitive Attenation
④Post Net

 また、図にはありませんがTacotron2に入力する前に省略表記(Mr、Missや、1st、2ndとか)を直す等の前処理が必要です。

f:id:akifukka:20191116212142j:plain

 Tactron2はエンコーダ・デコーダモデルです。エンコーダで入力データを解析、入力データを中間データに変換し、デコーダで中間データの情報を出力データに変換します。出力データ長を自在に変えられるのが特徴で、自動翻訳などで使われます。

2.NVIDIA実装

 NVIDIA実装を使ってTacotron2の仕組みを見ていきます。
 NVIDIA実装におけるTacotron2の主体は、model.pyのclass Tacotron2です。クラスTacotron2のinferenceを次に示します。

    def inference(self, inputs):
        embedded_inputs = self.embedding(inputs).transpose(1, 2)
        encoder_outputs = self.encoder.inference(embedded_inputs)
        mel_outputs, gate_outputs, alignments = self.decoder.inference(
            encoder_outputs)

        mel_outputs_postnet = self.postnet(mel_outputs)
        mel_outputs_postnet = mel_outputs + mel_outputs_postnet

        outputs = self.parse_output(
            [mel_outputs, mel_outputs_postnet, gate_outputs, alignments])

        return outputs

2.1 Character Embedding

 Embedding(日本語で組み込みの意味)はニューラルネットワークに入力できるよう、入力それぞれにテンソルを割り当てる処理のことを言います。今回の場合は65種類の入力(文字)にそれぞれ512次元のテンソルを割り当てます。テンソルの各項目の値を学習で変化させることで値に特徴の意味を持たせて分類することができます。例えば大文字のAと小文字のaでは実質同じとかです。

 Character EmbeddingはクラスTacotron2のinferenceの次の文で設定しています。

embedded_inputs = self.embedding(inputs).transpose(1, 2)

 この文はtorch.nn.embedding()を呼び出してembeddingの設定をします。transposeはテンソルの軸の入れ替えで、後で実行するConvolutional networkにあわせて軸(文字の順番とテンソルの次元)を入れ替えています。(1,文字数,512)→(1,512,文字数)。

 self.embeddingはclass Tacotron2 __init__で次のように定義されています。

self.embedding = nn.Embedding(
 hparams.n_symbols, hparams.symbols_embedding_dim)
std = sqrt(2.0 / (hparams.n_symbols + hparams.symbols_embedding_dim))
val = sqrt(3.0) * std # uniform bounds for std
self.embedding.weight.data.uniform_(-val, val)

 パラメータは次の通りです。
hparams.n_symbols = アルファベット大文字+小文字+記号類でたぶん65
hparams.symbols_embedding_dim = 512

参考

Word Embeddings: Encoding Lexical Semantics — PyTorch Tutorials 1.3.0 documentation

2.2 Encoder

 EncoderはクラスTacotron2のinferenceの次の文で処理しています。__init__でself.encoder = Encoder(hparams)を宣言しているので、次の文でクラスEncoderのinferrenceが呼び出されます。

encoder_outputs = self.encoder.inference(embedded_inputs)

 class Encoderのinferenceを次に示します。

    def inference(self, x):
        for conv in self.convolutions:
            x = F.dropout(F.relu(conv(x)), 0.5, self.training)

        x = x.transpose(1, 2)

        self.lstm.flatten_parameters()
        outputs, _ = self.lstm(x)

        return outputs

①3層Convolutionalネットワーク

 上記inference中のconvolutionsはclass Encoderの__init__で次のように定義されています。

for _ in range(hparams.encoder_n_convolutions):
            conv_layer = nn.Sequential(
                ConvNorm(hparams.encoder_embedding_dim,
                         hparams.encoder_embedding_dim,
                         kernel_size=hparams.encoder_kernel_size, stride=1,
                         padding=int((hparams.encoder_kernel_size - 1) / 2),
                         dilation=1, w_init_gain='relu'),
                nn.BatchNorm1d(hparams.encoder_embedding_dim))
            convolutions.append(conv_layer)

ここで、hparams.encoder_n_convolutions = 3です。
さらにConvNormはlayers.pyでクラスConvNormとして次のように定義されています。

self.conv = torch.nn.Conv1d(in_channels, out_channels,
  kernel_size=kernel_size, stride=stride,
  padding=padding, dilation=dilation,
  bias=bias)

 パラメータは次の通りです。
in_channels = 512, out_channels = 512, kernel_size = 5, stride = 1, padding = 2, dilation = 1, bias = True

 5 × 512のfilterを512種使ったConvolutional(折り畳みネットワーク)が3層定義されています。

 この3層Convolutionalネットワークで5文字の組み合わせ(filter)を使い発音の特徴を抽出しています。各層ごとにfilterは512種類あり、複雑な組み合わせにも対応できるようになっています。

 ここまでの処理を図にまとめると次のようになります。

f:id:akifukka:20191116130357j:plain

②Bydirectional LSTM(Long Short Term Memory)

 Bydirectional LSTMはクラスEncoderのinferenceの次の文で実行されます。

self.lstm.flatten_parameters()
outputs, _ = self.lstm(x)

 flatten_parametersはRNNのparameter datapointerをリセットして処理を高速化するための処理とのこと。

 self.lstmは__init__で次のように定義されています。赤字は解説用に追記しました。

self.lstm = nn.LSTM(hparams.encoder_embedding_dim = 512,
    int(hparams.encoder_embedding_dim / 2) = 256, 1,
    batch_first=True, bidirectional=True)

 pytoachのライブラリnn.LSTMを使っています。
 入力512次元、隠れ要素数256、層数1、双方向です。LSTMの出力テンソルの次元 = 隠れ要素数ですが、双方向なので×2して512次元のテンソルになります。

 LSTMの処理の概要の図を次に示します。

f:id:akifukka:20191116182920j:plain

 LSTMはニューロンのFF(フリップフロップ)のようなもので、過去の入力状態を保持することができ、それを使って状態分析を行うことができます。入力を保持する(INPUT GATE)、出力する(OUTPUT GATE)、リセット(FORGET GATE)の3つの制御信号をニューラルネットワークで制御して記憶を操作します。

 Tacotron2ではLSTMで離れた位置の文字と文字との関係を分析して発声に反映させることを可能にしているのではないかと思われます。

 なお、双方向LSTMを使っているので、前の文字だけではなく、後にくる文字の影響も汲みあげることができるようになっています。

  LSTMはRNN(Recurrent  Neural Network、再帰ニューラルネットワーク)を長期記憶できるよう改良したものです。自然言語分析、時系列データの分析といった用途で利用される強力な手法です。

次回、Decoderにつづきます。

 

音声合成 カテゴリーの記事一覧へ

ランニングことはじめ2

 この記事は2019年秋現在です。

  今回は、今実際に使っているものを中心にランニングに便利なグッヅを紹介します。ちなみに、毎週土曜は21km、日曜は10kmと、週2回のペースでランニングしています。

f:id:akifukka:20191117134710j:plain


1.シューズ

 何はともあれこれでしょう。前回も書いた通り、シューズは最初から全力投資すべきです。自分の経験から、安物買いは膝を痛めて結局短期間で買い替えになると思います。

 シューズにはいろいろありますが、最初はクッション性重視が全てです。今履いているうちのひとつ、ASICSのGEL NIMBUS20(20は2018年モデル)はその点では十分すぎるくらいです。

 膝を痛めた時に購入して以降、ひざが痛むことは無くなり今では週末に21kmを1時間50分くらいで走ることができます。今では、ASICSのLYTE RACER、NIKEのエピックリアクトフライニット2と他のシューズも使っていますが、いまでもちょっと脚に負担がかかったなと感じた日の次はGEL NIMBUSを使っています。

 GEL NIMBUS20

f:id:akifukka:20191117101127j:plain

 なお、シューズ購入の際は次にご注意を。

  • サイズは普段履きより1cm大き目を推奨します。自分は0.5cm大き目ですが、それでも指先が詰まる感じがします。次回は1cm大きいものにするつもりです。
  • 色々流行りもありますが、最初はクッション性が高く、癖のないものがいいと思います。NIKEの厚底シューズはちょっと癖があって、購入から半年たつけど、いまだにしっくりきません・・・。

 週2回程度のランニングだとシューズは1年くらいは十分持ちます。

2.ランニングポーチ

 スマホ、小銭入れ、ボトル入れにランニングポーチがあると便利です。今使っているのはamazonで買った2代目です。最初は探しまわってドンキホーテで購入したんですが、店ではなかなかいいものが見つからなくて。

AiRun Techランニングポーチ 

  コンパクトで、洗濯もでき、腰に巻く長さはマジックテープで容易に調整できます。マジックテープは十分強力で、かつゴムで伸び縮みするので体にフィットし、重宝しています。ただ次の2つだけ注意が必要です。

  • ボトルの揺れを防止するゴムバンドがボトルの袋のさらに上の方についていたのですが、タイガーのステンレスの魔法瓶(冷たいスポーツドリンク入り)を入れて走ったら15分くらいで切れました。最初見た時から切れそうな気がしてましたが・・・。今は近くのケーヨデーツーで伸縮マジックテープバンドを購入、ゴムバンドの代わりにしてからは全く問題なく使えてます。普通のペットボトル等であれば大丈夫だと思いますが・・・。
  • 洗濯する際はマジックテープ部分はくっついた状態にして洗濯する必要があります。さもないと他の洗濯物にくっつきます。

 この手の商品は入れ替えも早く、いいデザインのものが出ると次々同じような商品が出てきて改良されていくので、上記の欠点もすぐに解消されていくと思います。

 毎回洗濯しているので、少しづつほつれてきました。価格的には半年から1年くらいの消耗品かな。

3.スポーツサングラス

f:id:akifukka:20191117110059j:plain

 10kmとか走れるようになり、時間も1時間から2時間と増えてくると、太陽光の紫外線による目のダメージも無視できなくなってきます。実際夏は朝でも光が眩しい。太陽を見ないようにしても、地面から反射した紫外線が目に入り、気づかないうちにダメージが蓄積していきます。

 日中、ある程度の時間走る人は紫外線をカットするサングラスは必要だと思います。

4.ワイヤレスイヤホン

 音楽聞きながらランニング、また、無料のランニングアプリを使うと時間、距離、ペースを音声で教えてくれます。今使っているのはです。再生時間は最大10時間。最大時間は確かめてませんが7時間くらいは問題ありませんでした。ケースがバッテリー内蔵充電器になっていて、イヤホンをケースに収納すると充電が開始されます。ケース自体を充電するのにUSB-CタイプのACアダプタが別途必要です。

f:id:akifukka:20191117115802j:plain

 購入前は運動時に落ちないかとい不安もありましたが、結局ランニングくらいでは全く問題ありませんでした。音質も上々で、これ以上の音質を求めるなら、こんな手ごろな価格のものではなく高級オーディオ品から探すべきという感じです。

 ランニング時、通勤時にSpotifyを聞いてます。

音楽発見サービス - Spotify

 周囲の音が聞きずらくなるので、そこだけは気を付ける必要があります。

 5.ランニングアプリ

 ランニングアプリとしては以下の2つが有名です。機能的には無料版で十分だと思います。スマホにインストールして使います。GPSと連動して走ったコース、タイム、スプリットタイム、速さを記録してくれます。ランニング中に距離、時間、ペースを音声で教えてくれます。

 ASICS RUN KEEPER

www.asics.com

 NIKE  Run Club

www.nike.com

 RUN KEEPERから使い始め → RUN CLUBを試して→ 今は再びRUN KEEPERを使っています。RUN CLUBは半年使って2、3回、途中からあさってのコースを走ったことになっていました。(往復したのに、まっすぐいったことになっているとか)

 ただしスマホGPSの性能によるものかもしれません。

 RUN KEEPERは今までそこまでおかしな計測にはなったことがありませんが、180度ターンのようなコーナがあると少し短めに計測されている気がします。

 また、RUN KEEPERケイデンス(1分あたりの歩数)も計測してくれます。

6. 吸汗速乾シャツ

 ミズノの長袖トレーニングウェア 32JA6130 を着ています。すぐに乾くので汗で気持ち悪くなることがありません。同じような効果のものが色々あるようなのでお好みで。

7.ランニングソックス

 厚手でクッション性があり、丈夫で長持ちです。お勧め。 

 8.防寒対策

 普段は半袖(中に長袖トレーニングウェア)、ハーフパンツですが冬は防寒対策が必要です。ペースによりますが、走り始めると暑くなります。

(1)スポーツタイツ

 ハーフパンツの下に着用します。素足は寒いです。

(2)ウインドブレーカ

 薄手のジャケットだけでいいと思います。走り始めて暑くなったら脱いで腰に巻きます。

(3)手袋

 コンビニの手袋で・・・。走り始めたら脱いでます。手も汗をかくので容易に洗濯できるものがいいと思います。

9.暑さ対策

 夏は朝でも走るのは困難です。朝7時25~6℃くらいのところで走りはじめますが、普段の距離は走り切れず5~6kmすぎからトボトボ走りになってしまいます・・・。が、走らないと体重増えるし、走れなくなっていくし・・。

 休憩時、走り終えた後に合計1リッターくらいスポーツドリンク等をガブ飲みしますが、それでも走る前と比べて2kgくらい体重が減ります。水分なので、2,3日で戻りますが・・。

 夏は脱水、熱射病で倒れないように十分気を付けて。

(1)帽子

 無いと熱射病で倒れます。あっても死にそうです。ランニングのたびに洗濯機で洗ってます。洗わないと塩吹きます。

(2)日焼け止め

 日焼けに弱いのでこれも必須です。SPF50+を足、顔、手にぬってますが、それでも焼けます。

10.ボトル

 タイガーのステンレスマグボトル600mlを使っています。夏は氷をいれて冷えたスポーツドリンクを飲んでいますが、足りないので走り終わった直後、自販機で追加購入。

 ステンレスのマグボトルの代わりに500mlのペットボトルでも全く問題ありませんが、夏はやっぱり冷たいのがメチャ美味い。

 スポーツドリンクは近所のドラッグストアで購入してます。

11.ビタミンC

 ビタミンCは運動で発生する活性化酸素に対する抗酸化作用があります。走る前に飲んでますが、井藤漢方製薬のC1200は60袋600円とコストパフォーマンス高いです。以前は近所のドラッグストアにもあったのですが、取り扱われなくなってしまいました・・・。 

井藤漢方製薬 ビタミンC 1200 (60袋)

井藤漢方製薬 ビタミンC 1200 (60袋)

 

12.プロテイン

 ランニングは脂肪をエネルギーに変えますが、足りなくなると筋肉もエネルギーに変えてしまいます。ランニング直後にプロテインを飲むと、筋肉の回復を助けてくれるとのこと。確かにランニング直後に飲むと筋肉痛になりずらいような・・・・気がします。

 最初のうちはいらないと思います。ある程度走れるようになると効果があるかも。

13.まとめ

 以上、ランニングに便利なグッヅの紹介でした。ランニングウェアは紹介していませんが、これはお好みで! 

ランニングの帰りにいた鴨のつがい

f:id:akifukka:20191117134427j:plain

では。

ランニング カテゴリーの記事一覧へ

音声合成を試す2

8.解説

 色々調べたことをまとめました。間違っている箇所があれば気づき次第更新しますのでご容赦を。

(1)メル スペクトログラム(mel-spectrogram)
  以下のホームページで丁寧に解説されてます。

towardsdatascience.com

 以下はメル スペクトログラムの例です。”あ”を出しながら音の高さを上げていきました。縦軸は周波数、横軸は時間軸、色は周波数成分の音の強さです。

f:id:akifukka:20191109201446j:plain

 メル スペクトログラムはの特徴は次の通りです。

・各周波数成分の音の強さが、どう時間変化していくかを示すもの。なお周波数成分の位相情報は捨てています。

・周波数はメル スケールで抽出する。すなわち低い周波数はより細かく、高い周波数は間引いてサンプルします。これは人が高音は周波数の違いに鈍感で、低温は周波数の違いに敏感なためです。

 実装を見てみます。学習のためにメル スペクトグラムを生成する箇所の抜粋です。
layers.py Short-time Fourier transform (STFT)

class TacotronSTFT(torch.nn.Module):
    def __init__(self, filter_length=1024, hop_length=256, win_length=1024,
                 n_mel_channels=80, sampling_rate=22050, mel_fmin=0.0,
                 mel_fmax=8000.0):
 ・                 
        mel_basis = librosa_mel_fn(
            sampling_rate, filter_length, n_mel_channels, mel_fmin, mel_fmax)
            →librosa_mel_fn=librosa.filters.mel、メル周波数分布に変換するためのマトリクス係数を算出
 ・
 ・
    def mel_spectrogram(self, y):
        magnitudes, phases = self.stft_fn.transform(y)       fftで週波数成分の大きさ、位相を計算
        magnitudes = magnitudes.data                大きさを取り出し
        mel_output = torch.matmul(self.mel_basis, magnitudes)    メル周波数分布に変換
        mel_output = self.spectral_normalize(mel_output)
        return mel_output

サンプリング周波数 22.05ksps
1回のFFT数 filter_length 1024(22.05ksps時、46ms分のデータを1回のFFTで使用する)
ホップ数 hop_length 256 256サンプルづつずらしながらFFTする(11.6ms)
周波数範囲(mel_fmin~mel_fmax) 0~8kHz
メルchannel 80 上記8kHzをメルスケールで80分割するという意味

メル スペクトログラム配列の大きさは次のようになります。
 =メルchannel × (時間(s)×サンプリング周波数 / hop_length)

(2)発声の仕組み

 有声音(声帯から出る音を使って発する声)の場合、声帯から周期的に音源パルスを出して、喉、口で共鳴させて声を作ります。共鳴周波数は口の形、舌の位置等で変わり、メル スペクトログラムに特徴として現れます。

f:id:akifukka:20191110114936j:plain

(3)耳での音の周波数成分分解
 耳では蝸牛で音を周波数変換して、その時々の音の周波数成分ごとの強さを感じ取っています。蝸牛は実際はかたつむりのように巻いていますが、下の図はイメージをつかむために引き延ばして書いてあります。

f:id:akifukka:20191110115814j:plain

(4)声帯の発生音周波数と声道(喉、口)の共鳴スペクトラム

 (1)のメル スペクトログラムの明るい線はすべて声帯が出している発生音及びその高調波です。声道の特徴は一見してはこの図からはわかりません。先ほどのメルスペクトログラムから1s、4sを取り出したもの(スペクトラム)が下図です。

 細かい山谷は声帯の出す高調波で、なだらかな山谷(赤線)が声道の共鳴の特性です。細かい山谷は声の高さで変わっています(1sは低い声、4秒は高い声)が、赤い特徴はあまり変化しません。

f:id:akifukka:20191110142951j:plain

(5)ケプストラム

 上記のスペクトラムから、細かい山谷と、なだらかな山谷を分離できれば声帯と声道の共鳴スペクトラムを分離することができます。上記のスペクトラムをもう一回フーリエ変換したものをケプストラムと呼びます。

 ケプストラムから高い周波数成分(上記(4)の細かい山谷)をカットして、逆フーリエ変換して戻すと、声道の共鳴スペクトラム(赤線)だけ取り出すことができます。

9.実験

 少し実際に音声データを録音して実験してみます。

(1)wav録音、Googleドライブに保存

 どんなツールでもかまいませんが、wavファイルで録音します。特に手持ちがなければ、次のソフトは軽量で(インストールの必要がなく、ダウンロード容量も約240kBtyte)、かつパソコンのマイクから録音できてお勧めです。

www.vector.co.jp

・ファイル名 test1.wav
・周波数22kHz、チャンネル数モノラル、ビット数16bit、

として録音し、Googleドライブのtacotron2フォルダにアップロードします。

(2)Googleドライブのtacotron2フォルダで右クリックして、その他-Google Colaboratoryで新規ノートブックを立ち上げます。

(3)パッケージインストール

!pip install librosa
!pip install pillow

(4)Googlleドライブマウント

from google.colab import drive
drive.mount('/content/drive')

(5)パッケージのインポート

import matplotlib
import matplotlib.pylab as plt
import numpy as np
import librosa
from librosa import display

(6)Googleドライブのwavデータ読み込み

test1_data ,test1_rate = librosa.load("/content/drive/My Drive/tacotron2/test1.wav")

(7)波形表示

  波形を表示させてみます。赤字は拡大したいところの時間です。全体の波形を見て手動で設定します。波形はあいうえおを区切って発音したものです。

#全体
fig = plt.figure(figsize=(16, 20))
test1_time = np.arange(0, test1_data.shape[0]/test1_rate, 1/test1_rate) #x軸の値(データ数から時間を計算)
fig_1 = fig.add_subplot(6,1,1)
fig_1.plot(test1_time,test1_data)

#拡大 あ
fig_2 = fig.add_subplot(6,1,2)
fig_2.plot(test1_time,test1_data)
fig_2.set_xlim(1.1,1.2)

#拡大 い
fig_3 = fig.add_subplot(6,1,3)
fig_3.plot(test1_time,test1_data)
fig_3.set_xlim(1.8,1.9)

#拡大 う
fig_4 = fig.add_subplot(6,1,4)
fig_4.plot(test1_time,test1_data)
fig_4.set_xlim(2.5,2.6)

#拡大 え
fig_5 = fig.add_subplot(6,1,5)
fig_5.plot(test1_time,test1_data)
fig_5.set_xlim(3.2,3.3)

#拡大 お
fig_6 = fig.add_subplot(6,1,6)
fig_6.plot(test1_time,test1_data)
fig_6.set_xlim(3.8,3.9)

fig.show

 f:id:akifukka:20191110211045j:plain

(8)メル スペクトグラム

 メル スペクトログラムを表示します。

#mel spectrogram
S = librosa.feature.melspectrogram(test1_data, sr=test1_rate, n_mels=128)
S_dB = librosa.power_to_db(S, ref=np.max)

plt.figure(figsize=(12, 4))
librosa.display.specshow(S_dB, sr=test1_rate, x_axis='time', y_axis='mel')

plt.title('Mel-frequency spectrogram')
plt.colorbar(format='%+2.0f dB')
plt.tight_layout()
plt.show

#mel スペクトラム
mel_x = np.arange(0, 128, 1) #x軸を生成、単純数列
spec_t=[1.2,1.8,2.5,3.2,3.8]
for t in spec_t:
  plt.figure(figsize=(12, 4))
  t = int(t/(512/22050)) #1.1s時点でのmelスペクトラム
  plt.plot(mel_x,S_dB.T[t])
  plt.xlabel("mel")
  plt.ylabel("dB")
  plt.show

#melに相当する周波数の計算(参考)
print("mel 20=",librosa.mel_to_hz((20.0/128.0*librosa.hz_to_mel(8000))))
print("mel 40=",librosa.mel_to_hz((40.0/128.0*librosa.hz_to_mel(8000))))
print("mel 60=",librosa.mel_to_hz((60.0/128.0*librosa.hz_to_mel(8000))))
print("mel 80=",librosa.mel_to_hz((80.0/128.0*librosa.hz_to_mel(8000))))
print("mel 100=",librosa.mel_to_hz((100.0/128.0*librosa.hz_to_mel(8000))))

  あいうえおと発音した時のメル スペクトログラムです。細い線は声帯の周波数成分(高調波)です。わかりやすいように、各発声時のスペクトラムも表示しました。

f:id:akifukka:20191110211502j:plain

(9)ケプストラム1

 ケプストラムを計算した後、逆変換してメル スペクトグラムに戻します。ほぼ元の形に戻りました。

#mel cepstral
mfccs = librosa.feature.mfcc(test1_data, sr=test1_rate, n_mfcc=128)
plt.figure(figsize=(10, 4))
librosa.display.specshow(mfccs, x_axis='time')
plt.colorbar()
plt.title('MFCC')
plt.tight_layout()
plt.show()

#mfcc to mel
S_dB_from_mfccs = scipy.fftpack.idct(mfccs, axis=0, type=2, norm='ortho', n=128)
plt.figure(figsize=(12, 4))
librosa.display.specshow(S_dB_from_mfccs, sr=test1_rate, x_axis='time', y_axis='mel')

plt.title('Mel-frequency spectrogram')
plt.colorbar(format='%+2.0f dB')
plt.tight_layout()
plt.show

#mel スペクトラム
mel_x = np.arange(0, 128, 1) #x軸を生成、単純数列
spec_t=[1.2,1.8,2.5,3.2,3.8]
for t in spec_t:
  plt.figure(figsize=(12, 4))
  t = int(t/(512/22050)) #1.1s時点でのmelスペクトラム
  plt.plot(mel_x,S_dB_from_mfccs.T[t])
  plt.xlabel("mel")
  plt.ylabel("dB")
  plt.show

 f:id:akifukka:20191110215943j:plain


(10)ケプルトラム2

 ケプストラムのうち高域をカットしてメル スペクトログラムに戻します。声帯の高調波成分がカットされていることがわかります。変更したのは赤字のところ1か所(低域から数えていくつの成分まで返り値とするかの設定)だけです。

#mel cepstral
mfccs = librosa.feature.mfcc(test1_data, sr=test1_rate, n_mfcc=20)
・
・
後は同じ

 スペクトラムの細かい山谷が無くなって声道の特性だけを取り出せていることがわかります。ケプストラムの高調波分を無くすということはローパスフィルタを通すのと同様の効果があります。

f:id:akifukka:20191110214629j:plain

 ここまでのノートブックのpdfを参考に貼っておきます。

https://drive.google.com/file/d/1_Fvk5tWHgBLKrgJSoRTzBCdBKF1D0okf/view?usp=sharing

 

試すのはいったんおしまいです。

次回以降はtacotron2の中身を調べていきたいと思います。

では!

音声合成 カテゴリーの記事一覧へ

音声合成を試す1 Tacotron2 + WaveGlow

 ディープラーニングによって音声合成も目覚ましく進歩しているようです。
 2019年4月に発表されたマイクロソフトAIりんなの歌の完成度は非常に高く、人と区別つきません。まだ、誰もが使える技術ではありませんが、いつかは誰もがつかえるようになるでしょう。

www.youtube.com

 とりあえず、試せる範囲で音声合成技術に触れてみたいと思います。

1.Text to speechの概要

 テキストから音声への変換はText to speechと呼びます。ここではTacotron2 + WaveglowのNVIDIA実装を使ってみます。ソースのリンクはこちら。

github.com

Tacotron2
 2017年にGoogleが発表したTTSの手法。Tacotron2はテキストからメル スペクトログラム(mel-spectrogram)を生成します。
 論文のリンクはこちらです。

arxiv.org

Waveglow
 メル スペクトログラムから音声波形への変換を行います。この分野では高品質な音声出力で有名なWaveNetに比べかなりの軽量化がされているにもかかわらず、品質も良好とのこと。論文はこちら。

arxiv.org

 詳しくは後にして、まずはGoogle Colaboratoryで動かしてみましょう。

 手順はNVIDIAのサイトにあるREADME.md、inference.ipynbをベースにしています。そのままでは動かなかったので一部変更しました。今後も、Colaboratory、Tacotron2、WaveGlowが更新されるとエラーが出るようになるかも知れません・・・。

2.Googleドライブに学習済みデータをアップロードする

(1)Googleドライブのトップにtacotron2フォルダを作成します。

(2)パソコンに次をダウンロードします。

・tacotron2学習済みweights tacotron2_statedict.pt 約108MByte
https://drive.google.com/file/d/1c5ZTuT7J08wLUoVZ2KkUs_VdZuJ86ZqA/view?usp=sharing

・WaveGlow学習済みweights waveglow_256channels_ljs_v3.pt 約644MByte
https://ngc.nvidia.com/catalog/models/nvidia:waveglow_ljs_256channels

(3)上記2つのファイルをGoogleドライブのtacotron2フォルダにアップロードします(ドラッグ&ドロップ)。やや時間がかかります。

(4)Googleドライブで右クリックして新規Colaboratoryを開きます。

(5)ランタイム-ランタイムのタイプを変更でハードウェアアクセラレータをGPUにします。

3.Gitダウンロード

 今回はソースファイルをGoogleドライブに置かないで毎回Gitからダウンロードすることにしました。

(1)tacotron2

!git clone https://github.com/NVIDIA/tacotron2.git

(2)submodule(waveglowなど)導入
waveglowを最新版にしないと後でdenoiserが無いというエラーが出るので、--remote --mergeで最新版に更新しています。

%cd /content/tacotron2
!git submodule init
!git submodule update --remote --merge

4.Apexインストール

!git clone https://github.com/NVIDIA/apex
%cd apex
!pip install -v --no-cache-dir ./
%cd ..

5.各種パッケージインストール

(1)パッケージインストール

 とりあえず、以下の組み合わせで動きました。

%cd /content/tacotron2
!pip install matplotlib==3.0.0
!pip install tensorflow
!pip install inflect==0.2.5
!pip install librosa==0.6.0
!pip install scipy==1.0.0
!pip install tensorboardX==1.8
!pip install Unidecode==1.0.22
!pip install pillow
!pip install torch==1.0
!pip install torchvision==0.3.0

(2)ランタイム再起動
 ランタイムーランタイムを再起動で再起動します。ファイル等には影響ありません。

6.学習データをコピー

 2.項でGoogleドライブのtacotron2フォルダにアップロードした学習済みweightsをColaboratoryのディスクにコピーします。

(1)Googleドライブ接続

from google.colab import drive
drive.mount('/content/drive')

(2)学習データコピー

!cp /content/drive/My\ Drive/tacotron2/tacotron2_statedict.pt /content/tacotron2/
!cp /content/drive/My\ Drive/tacotron2/waveglow_256channels_ljs_v3.pt /content/tacotron2/

 7.実行します

(1)ディレクト

%cd /content/tacotron2

(2)ライブラリのインポート

import matplotlib
%matplotlib inline
import matplotlib.pylab as plt

import IPython.display as ipd

import sys
sys.path.append('waveglow/')
import numpy as np
import torch

from hparams import create_hparams
from model import Tacotron2
from layers import TacotronSTFT, STFT
from audio_processing import griffin_lim
from train import load_model
from text import text_to_sequence
from denoiser import Denoiser

(3)plot_data関数

def plot_data(data, figsize=(16, 4)):
    fig, axes = plt.subplots(1, len(data), figsize=figsize)
    for i in range(len(data)):
        axes[i].imshow(data[i], aspect='auto', origin='bottom', 
                       interpolation='none')

(4)Setup hparams設定 サンプリングレート 22.05k

hparams = create_hparams()
hparams.sampling_rate = 22050

(5)Load model tacotron2

checkpoint_path = "tacotron2_statedict.pt"
model = load_model(hparams)
model.load_state_dict(torch.load(checkpoint_path)['state_dict'])
_ = model.cuda().eval().half()

(6)Load WaveGlow for mel2audio synthesis and denoiser

#waveglow_path = 'waveglow_256channels.pt'
waveglow_path = 'waveglow_256channels_ljs_v3.pt'
waveglow = torch.load(waveglow_path)['model']
waveglow = waveglow.remove_weightnorm(waveglow)
waveglow.cuda().eval().half()
for k in waveglow.convinv:
    k.float()
denoiser = Denoiser(waveglow).cuda()

(7)Prepare text input

text = "Waveglow is really awesome!"
sequence = np.array(text_to_sequence(text, ['english_cleaners']))[None, :]
sequence = torch.autograd.Variable(
    torch.from_numpy(sequence)).cuda().long()

(8)Decode text input and plot results メルスペクトログラムを生成します

mel_outputs, mel_outputs_postnet, _, alignments = model.inference(sequence)
plot_data((mel_outputs.float().data.cpu().numpy()[0],
           mel_outputs_postnet.float().data.cpu().numpy()[0],
           alignments.float().data.cpu().numpy()[0].T))

 生成されたメル スペクトログラムが表示されます。左側がpost-processing無し、真ん中がpost-processing後(最終出力)です。この図ではあまり違いは良くわかりません。

f:id:akifukka:20191109150303j:plain

(9)Synthesize audio from spectrogram using WaveGlow メルスペクトログラムから音声波形を生成します

with torch.no_grad():
    audio = waveglow.infer(mel_outputs_postnet, sigma=0.666)
ipd.Audio(audio[0].data.cpu().numpy(), rate=hparams.sampling_rate)


再生すると音声が出ます ”Waveglow is really awesome!"

f:id:akifukka:20191109160333j:plain
 ここまでのノートブックを貼っておきます。

https://drive.google.com/file/d/1UPdfws9DJqrp-HYa7TOHmORplJUiuHYV/view?usp=sharing

 

つづく

音声合成 カテゴリーの記事一覧へ