第10章 日本語Tacotronに基づく音声合成システムの実装

Open In Colab

Google colabでの実行における推定所要時間: 5時間

このノートブックに記載のレシピの設定は、Google Colab上で実行した場合のタイムアウトを避けるため、学習条件を書籍に記載の設定から一部修正していることに注意してください (バッチサイズを減らす等)。 参考までに、書籍に記載の条件で、著者 (山本) がレシピを実行した結果を以下で公開しています。

準備

Google Colabを利用する場合

Google Colab上でこのノートブックを実行する場合は、メニューの「ランタイム -> ランタイムのタイムの変更」から、「ハードウェア アクセラレータ」を GPU に変更してください。

Python version

[1]:
!python -VV
Python 3.8.6 | packaged by conda-forge | (default, Dec 26 2020, 05:05:16)
[GCC 9.3.0]

ttslearn のインストール

[2]:
%%capture
try:
    import ttslearn
except ImportError:
    !pip install ttslearn
[3]:
import ttslearn
ttslearn.__version__
[3]:
'0.2.1'

10.1 本章の日本語音声合成システムの実装

学習済みモデルを用いた音声合成

[4]:
from ttslearn.tacotron import Tacotron2TTS
from tqdm.notebook import tqdm
from IPython.display import Audio

engine = Tacotron2TTS()
wav, sr = engine.tts("一貫学習にチャレンジしましょう!", tqdm=tqdm)
Audio(wav, rate=sr)
[4]:
[5]:
import librosa.display
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(8,2))
librosa.display.waveplot(wav.astype(np.float32), sr, ax=ax)
ax.set_xlabel("Time [sec]")
ax.set_ylabel("Amplitude")
plt.tight_layout()
../_images/notebooks_ch10_Recipe-Tacotron_12_0.png

レシピ実行の前準備

[6]:
%%capture
from ttslearn.env import is_colab
from os.path import exists

# pip install ttslearn ではレシピはインストールされないので、手動でダウンロード
if is_colab() and not exists("recipes.zip"):
    !curl -LO https://github.com/r9y9/ttslearn/releases/download/v{ttslearn.__version__}/recipes.zip
    !unzip -o recipes.zip
[7]:
import os
# recipeのディレクトリに移動
cwd = os.getcwd()
if cwd.endswith("notebooks"):
    os.chdir("../recipes/tacotron/")
elif is_colab():
    os.chdir("recipes/tacotron/")
[8]:
import time
start_time = time.time()

パッケージのインポート

[9]:
%pylab inline
%load_ext autoreload
%load_ext tensorboard
%autoreload
import IPython
from IPython.display import Audio
import tensorboard as tb
import os
Populating the interactive namespace from numpy and matplotlib
[10]:
# 数値演算
import numpy as np
import torch
from torch import nn
# 音声波形の読み込み
from scipy.io import wavfile
# フルコンテキストラベル、質問ファイルの読み込み
from nnmnkwii.io import hts
# 音声分析
import pyworld
# 音声分析、可視化
import librosa
import librosa.display
import pandas as pd
# Pythonで学ぶ音声合成
import ttslearn
[11]:
# シードの固定
from ttslearn.util import init_seed
init_seed(773)
[12]:
torch.__version__
[12]:
'1.8.1'

描画周りの設定

[13]:
from ttslearn.notebook import get_cmap, init_plot_style, savefig
cmap = get_cmap()
init_plot_style()

レシピの設定

[14]:
# run.shを利用した学習スクリプトをnotebookから行いたい場合は、True
# google colab の場合は、True とします
# ローカル環境の場合、run.sh をターミナルから実行することを推奨します。
# その場合、このノートブックは可視化・学習済みモデルのテストのために利用します。
run_sh = is_colab()

# 注意: WaveNetを利用した評価データに対する音声生成は時間がかかることに注意
run_stage6 = True

# run.sh経由で実行するスクリプトのtqdm
run_sh_tqdm = "none"

# CUDA
# NOTE: run.shの引数として渡すので、boolではなく文字列で定義しています
cudnn_benchmark = "true"
cudnn_deterministic = "false"

# 特徴抽出時の並列処理のジョブ数
n_jobs = os.cpu_count()//2

# 音響モデル (Tacotron) の設定ファイル名
acoustic_config_name="tacotron2_rf2"
# WaveNetボコーダの設定ファイル名
wavenet_config_name="wavenet_sr16k_mulaw256_30layers"

# Tacotron学習におけるバッチサイズ
tacotron_batch_size = 16
# Tacotron学習のイテレーション数
# 注意: 十分な品質を得るために必要な値: 50k ~ 100k steps
tacotron_max_train_steps = 5000

# WaveNetボコーダの学習におけるバッチサイズ
# 推奨バッチサイズ:  8以上
# 動作確認のため、小さな値に設定しています
wavenet_batch_size = 4
# WavaNetの学習イテレーション数
# 注意: 十分な品質を得るために必要な値: 300k ~ 500k steps
wavenet_max_train_steps = 20000

# 音声生成を行う発話数
# WaveNetの推論は時間がかかるので、ノートブックで表示する5つのみ生成する
num_eval_utts = 5

# ノートブックで利用するテスト用の発話(学習データ、評価データ)
train_utt = "BASIC5000_0001"
test_utt = "BASIC5000_5000"

Tensorboard によるログの可視化

[15]:
# ノートブック上から tensorboard のログを確認する場合、次の行を有効にしてください
if is_colab():
    %tensorboard --logdir tensorboard/

10.2 Tacotron 2 を日本語に適用するための変更

音素列と韻律記号付き音素列の比較

[16]:
import pyopenjtalk
# この実装は後述します
from ttslearn.tacotron.frontend.openjtalk import pp_symbols
[17]:
print("音素列:", pyopenjtalk.g2p("端が"))
print("音素列:", pyopenjtalk.g2p("箸が"))
print("音素列:", pyopenjtalk.g2p("橋が"))
音素列: h a sh i g a
音素列: h a sh i g a
音素列: h a sh i g a
[18]:
print("韻律記号付き音素列:", " ".join(pp_symbols(pyopenjtalk.extract_fullcontext("端が"))))
print("韻律記号付き音素列:", " ".join(pp_symbols(pyopenjtalk.extract_fullcontext("箸が"))))
print("韻律記号付き音素列:", " ".join(pp_symbols(pyopenjtalk.extract_fullcontext("橋が"))))
韻律記号付き音素列: ^ h a [ sh i g a $
韻律記号付き音素列: ^ h a ] sh i g a $
韻律記号付き音素列: ^ h a [ sh i ] g a $

フルコンテキストラベルからの音素列および韻律記号の抽出

[19]:
import re

def numeric_feature_by_regex(regex, s):
    match = re.search(regex, s)
    # 未定義 (xx) の場合、コンテキストの取りうる値以外の適当な値
    if match is None:
        return -50
    return int(match.group(1))
[20]:
labels = hts.load(ttslearn.util.example_label_file())
labels.contexts[1]
[20]:
'xx^sil-m+i=z/A:-2+1+3/B:xx-xx_xx/C:02_xx+xx/D:13+xx_xx/E:xx_xx!xx_xx-xx/F:3_3#0_xx@1_5|1_23/G:7_2%0_xx_1/H:xx_xx/I:5-23@1+1&1-5|1+23/J:xx_xx/K:1+5-23'
[21]:
numeric_feature_by_regex(r"/A:([0-9\-]+)\+", labels.contexts[1])
[21]:
-2
[22]:
def pp_symbols(labels, drop_unvoiced_vowels=True):
    PP = []
    N = len(labels)

    # 各音素毎に順番に処理
    for n in range(N):
        lab_curr = labels[n]

        # 当該音素
        p3 = re.search(r"\-(.*?)\+", lab_curr).group(1)

        # 無声化母音を通常の母音として扱う
        if drop_unvoiced_vowels and p3 in "AEIOU":
            p3 = p3.lower()

        # 先頭と末尾の sil のみ例外対応
        if p3 == "sil":
            assert n == 0 or n == N - 1
            if n == 0:
                PP.append("^")
            elif n == N - 1:
                # 疑問系かどうか
                e3 = numeric_feature_by_regex(r"!(\d+)_", lab_curr)
                if e3 == 0:
                    PP.append("$")
                elif e3 == 1:
                    PP.append("?")
            continue
        elif p3 == "pau":
            PP.append("_")
            continue
        else:
            PP.append(p3)

        # アクセント型および位置情報(前方または後方)
        a1 = numeric_feature_by_regex(r"/A:([0-9\-]+)\+", lab_curr)
        a2 = numeric_feature_by_regex(r"\+(\d+)\+", lab_curr)
        a3 = numeric_feature_by_regex(r"\+(\d+)/", lab_curr)
        # アクセント句におけるモーラ数
        f1 = numeric_feature_by_regex(r"/F:(\d+)_", lab_curr)

        a2_next = numeric_feature_by_regex(r"\+(\d+)\+", labels[n + 1])

        # アクセント句境界
        if a3 == 1 and a2_next == 1:
            PP.append("#")
        # ピッチの立ち下がり(アクセント核)
        elif a1 == 0 and a2_next == a2 + 1 and a2 != f1:
            PP.append("]")
        # ピッチの立ち上がり
        elif a2 == 1 and a2_next == 2:
            PP.append("[")

    return PP
[23]:
import pyopenjtalk

text = "今日の天気は?"

# テキストからフルコンテキストを抽出
labels = pyopenjtalk.extract_fullcontext(text)
# フルコンテキストから、韻律記号付き音素列に変換
PP = pp_symbols(labels)

print("入力文字列:", text)
print("音素列:", pyopenjtalk.g2p(text))
print("韻律記号付き音素列:", " ".join(PP))
入力文字列: 今日の天気は?
音素列: ky o o n o t e N k i w a
韻律記号付き音素列: ^ ky o ] o n o # t e ] N k i w a ?

プログラム実装の前準備

stage -1: コーパスのダウンロード

[24]:
if is_colab():
    ! ./run.sh --stage -1 --stop-stage -1

Stage 0: 学習/検証/評価データの分割

[25]:
if run_sh:
    ! ./run.sh --stage 0 --stop-stage 0
[26]:
! ls data/
dev.list  eval.list  train.list  utt_list.txt
[27]:
! head data/dev.list
BASIC5000_4701
BASIC5000_4702
BASIC5000_4703
BASIC5000_4704
BASIC5000_4705
BASIC5000_4706
BASIC5000_4707
BASIC5000_4708
BASIC5000_4709
BASIC5000_4710

10.3 データの前処理

Tacotron 2 のための前処理

1発話に対する前処理

[28]:
from ttslearn.tacotron.frontend.openjtalk import text_to_sequence, pp_symbols
from ttslearn.dsp import mulaw_quantize, logmelspectrogram

# 韻律記号付き音素列の抽出
labels = hts.load(ttslearn.util.example_label_file())
PP = pp_symbols(labels.contexts)
in_feats = np.array(text_to_sequence(PP), dtype=np.int64)

# メルスペクトログラムの計算
sr = 16000
_sr, x = wavfile.read(ttslearn.util.example_audio_file())
x = (x / 32768).astype(np.float64)
x = librosa.resample(x, _sr, sr)

out_feats = logmelspectrogram(x, sr)

# 冒頭と末尾の非音声区間の長さを調整
assert "sil" in labels.contexts[0] and "sil" in labels.contexts[-1]
start_frame = int(labels.start_times[1] / 125000)
end_frame = int(labels.end_times[-2] / 125000)

# 最初:50 ミリ秒、最後:100 ミリ秒
start_frame = max(0, start_frame - int(0.050 / 0.0125))
end_frame = min(len(out_feats), end_frame + int(0.100 / 0.0125))

out_feats = out_feats[start_frame:end_frame]

# 時間領域で音声の長さを調整
x = x[int(start_frame * 0.0125 * sr) :]
length = int(sr * 0.0125) * out_feats.shape[0]
x = pad_1d(x, length) if len(x) < length else x[:length]

# 特徴量のアップサンプリングを行う都合上、音声波形の長さはフレームシフトで割り切れる必要があります
assert len(x) % int(sr * 0.0125) == 0

# mu-law 量子化
x = mulaw_quantize(x)
[29]:
print("Tacotron の入力特徴量のサイズ:", in_feats.shape)
print("Tacotron の出力特徴量のサイズ:", out_feats.shape)
print("WaveNet ボコーダの出力の音声波形のサイズ:", x.shape)
Tacotron の入力特徴量のサイズ: (56,)
Tacotron の出力特徴量のサイズ: (227, 80)
WaveNet ボコーダの出力の音声波形のサイズ: (45400,)
[30]:
from ttslearn.tacotron.frontend.openjtalk import num_vocab
from ttslearn.dsp import inv_mulaw_quantize
from torch.nn import functional as F

inp = F.one_hot(torch.from_numpy(in_feats), num_vocab()).numpy()

fig, ax = plt.subplots(3, 1, figsize=(8,8))
ax[0].set_title("Phoneme sequence + prosody symbols (one-hot)")
ax[1].set_title("Mel-spectrogram")
ax[2].set_title("Mu-law quantized waveform")

ax[0].imshow(inp.T, aspect="auto", interpolation="nearest", origin="lower", cmap=cmap)
ax[1].imshow(out_feats.T, aspect="auto", interpolation="nearest", origin="lower", cmap=cmap)
librosa.display.waveplot(x.astype(np.float32), ax=ax[2], sr=sr)

ax[0].set_xlabel("Phoneme")
ax[0].set_ylabel("Binary value")
ax[1].set_xlabel("Time [frame]")
ax[1].set_ylabel("Mel filter channel")
ax[2].set_xlabel("Time [sec]")
ax[2].set_ylabel("Amplitude")

plt.tight_layout()
savefig("fig/e2etts_impl_taco2_inout")
../_images/notebooks_ch10_Recipe-Tacotron_51_0.png

レシピの stage 1 の実行

バッチ処理を行うコマンドラインプログラムは、 preprocess.py を参照してください。

[31]:
if run_sh:
    ! ./run.sh --stage 1 --stop-stage 1

特徴量の正規化

正規化のための統計量を計算するコマンドラインプログラムは、 recipes/common/fit_scaler.py を参照してください。また、正規化を行うコマンドラインプログラムは、 recipes/common/preprocess_normalize.py を参照してください。

レシピの stage 2 の実行

[32]:
if run_sh:
    ! ./run.sh --stage 2 --stop-stage 2 --n-jobs $n_jobs

正規化の処理の結果の確認

[33]:
in_feats = np.load(f"dump/jsut_sr16000/org/train/out_tacotron/{train_utt}-feats.npy")
in_feats_norm = np.load(f"dump/jsut_sr16000/norm/train/out_tacotron/{train_utt}-feats.npy")
fig, ax = plt.subplots(2, 1, figsize=(8,6), sharex=True)
ax[0].set_title("Mel-spectrogram (before normalization)")
ax[1].set_title("Mel-spectrogram (after normalization)")

hop_length = int(sr * 0.0125)
mesh = librosa.display.specshow(
    in_feats.T, sr=sr, hop_length=hop_length, x_axis="time", y_axis="frames", ax=ax[0], cmap=cmap)
fig.colorbar(mesh, ax=ax[0])
mesh = librosa.display.specshow(
    in_feats_norm.T, sr=sr, hop_length=hop_length, x_axis="time", y_axis="frames",ax=ax[1], cmap=cmap)
mesh.set_clim(-4, 4)
fig.colorbar(mesh, ax=ax[1])

for a in ax:
    a.set_xlabel("Time [sec]")
    a.set_ylabel("Mel filter channel")
plt.tight_layout()
../_images/notebooks_ch10_Recipe-Tacotron_59_0.png

10.4 Tacotron の学習スクリプトの作成

DataLoader の実装

collate_fn の実装

[34]:
def ensure_divisible_by(feats, N):
    if N == 1:
        return feats
    mod = len(feats) % N
    if mod != 0:
        feats = feats[: len(feats) - mod]
    return feats
[35]:
from ttslearn.util import pad_1d, pad_2d

def collate_fn_tacotron(batch, reduction_factor=1):
    xs = [x[0] for x in batch]
    ys = [ensure_divisible_by(x[1], reduction_factor) for x in batch]
    in_lens = [len(x) for x in xs]
    out_lens = [len(y) for y in ys]
    in_max_len = max(in_lens)
    out_max_len = max(out_lens)
    x_batch = torch.stack([torch.from_numpy(pad_1d(x, in_max_len)) for x in xs])
    y_batch = torch.stack([torch.from_numpy(pad_2d(y, out_max_len)) for y in ys])
    in_lens = torch.tensor(in_lens, dtype=torch.long)
    out_lens = torch.tensor(out_lens, dtype=torch.long)
    stop_flags = torch.zeros(y_batch.shape[0], y_batch.shape[1])
    for idx, out_len in enumerate(out_lens):
        stop_flags[idx, out_len - 1 :] = 1.0
    return x_batch, in_lens, y_batch, out_lens, stop_flags

DataLoader の利用例

[36]:
from pathlib import Path
from ttslearn.train_util import Dataset, collate_fn_tacotron
from functools import partial

in_paths = sorted(Path("./dump/jsut_sr16000/norm/dev/in_tacotron/").glob("*.npy"))
out_paths = sorted(Path("./dump/jsut_sr16000/norm/dev/out_tacotron/").glob("*.npy"))

dataset = Dataset(in_paths, out_paths)
collate_fn = partial(collate_fn_tacotron, reduction_factor=1)
data_loader = torch.utils.data.DataLoader(dataset, batch_size=8, collate_fn=collate_fn, num_workers=0)

in_feats, in_lens, out_feats, out_lens, stop_flags = next(iter(data_loader))
print("入力特徴量のサイズ:", tuple(in_feats.shape))
print("出力特徴量のサイズ:", tuple(out_feats.shape))
print("stop flags のサイズ:", tuple(stop_flags.shape))
入力特徴量のサイズ: (8, 66)
出力特徴量のサイズ: (8, 286, 80)
stop flags のサイズ: (8, 286)

ミニバッチの可視化

[37]:
fig, ax = plt.subplots(len(out_feats), 1, figsize=(8,10), sharex=True, sharey=True)
for n in range(len(in_feats)):
    x = out_feats[n].data.numpy()
    hop_length = int(sr * 0.0125)
    mesh = librosa.display.specshow(x.T, sr=sr, x_axis="time", y_axis="frames", hop_length=hop_length, cmap=cmap, ax=ax[n])
    fig.colorbar(mesh, ax=ax[n])
    mesh.set_clim(-4, 4)
    # あとで付け直すので、ここではラベルを削除します
    ax[n].set_xlabel("")

ax[-1].set_xlabel("Time [sec]")
for a in ax:
    a.set_ylabel("Mel channel")

plt.tight_layout()
savefig("fig/e2etts_impl_minibatch")
../_images/notebooks_ch10_Recipe-Tacotron_68_0.png

簡易的な学習スクリプトの実装

学習の前準備

[38]:
from ttslearn.tacotron import Tacotron2 as Tacotron
from torch import optim

# 動作確認用:層の数を減らした小さなTacotron
model = Tacotron(
    embed_dim=32, encoder_conv_layers=1, encoder_conv_channels=32, encoder_hidden_dim=32,
    decoder_hidden_dim=32, postnet_channels=32, postnet_layers=1)

# lr は学習率を表します
optimizer = optim.Adam(model.parameters(), lr=0.001)

# gamma は学習率の減衰係数を表します
lr_scheduler = optim.lr_scheduler.StepLR(optimizer, gamma=0.5, step_size=100000)

学習ループの実装

[39]:
from ttslearn.util import make_non_pad_mask

# DataLoader を用いたミニバッチの作成: ミニバッチ毎に処理する
for in_feats, in_lens, out_feats, out_lens, stop_flags in tqdm(data_loader):
    in_lens, indices = torch.sort(in_lens, dim=0, descending=True)
    in_feats, out_feats, out_lens = in_feats[indices], out_feats[indices], out_lens[indices]

    # 順伝搬の計算
    outs, outs_fine, logits, _ = model(in_feats, in_lens, out_feats)

    # ゼロパディグした部分を損失関数のの計算から除外するためにマスクを適用します
    # Mask (B x T x 1)
    mask = make_non_pad_mask(out_lens).unsqueeze(-1)
    out_feats = out_feats.masked_select(mask)
    outs = outs.masked_select(mask)
    outs_fine = outs_fine.masked_select(mask)
    stop_flags = stop_flags.masked_select(mask.squeeze(-1))
    logits = logits.masked_select(mask.squeeze(-1))

    # 損失の計算
    decoder_out_loss = nn.MSELoss()(outs, out_feats)
    postnet_out_loss = nn.MSELoss()(outs_fine, out_feats)
    stop_token_loss = nn.BCEWithLogitsLoss()(logits, stop_flags)

    # 損失の合計
    loss = decoder_out_loss + postnet_out_loss + stop_token_loss

    # 損失の値を出力
    print(f"decoder_out_loss: {decoder_out_loss:.2f}, postnet_out_loss: {postnet_out_loss:.2f}, stop_token_loss: {stop_token_loss:.2f}")
    # optimizer に蓄積された勾配をリセット
    optimizer.zero_grad()
    # 誤差の逆伝播
    loss.backward()
    # パラメータの更新
    optimizer.step()
    # 学習率スケジューラの更新
    lr_scheduler.step()
decoder_out_loss: 1.06, postnet_out_loss: 3.13, stop_token_loss: 0.75
decoder_out_loss: 1.01, postnet_out_loss: 3.07, stop_token_loss: 0.72
decoder_out_loss: 1.00, postnet_out_loss: 3.03, stop_token_loss: 0.73
decoder_out_loss: 0.98, postnet_out_loss: 3.17, stop_token_loss: 0.72
decoder_out_loss: 0.98, postnet_out_loss: 2.97, stop_token_loss: 0.70
decoder_out_loss: 0.95, postnet_out_loss: 2.87, stop_token_loss: 0.70
decoder_out_loss: 0.96, postnet_out_loss: 2.96, stop_token_loss: 0.69
decoder_out_loss: 0.99, postnet_out_loss: 2.80, stop_token_loss: 0.68
decoder_out_loss: 0.98, postnet_out_loss: 2.84, stop_token_loss: 0.68
decoder_out_loss: 0.97, postnet_out_loss: 2.84, stop_token_loss: 0.66
decoder_out_loss: 1.00, postnet_out_loss: 2.83, stop_token_loss: 0.66
decoder_out_loss: 1.02, postnet_out_loss: 2.87, stop_token_loss: 0.65
decoder_out_loss: 1.07, postnet_out_loss: 2.67, stop_token_loss: 0.64
decoder_out_loss: 1.04, postnet_out_loss: 2.60, stop_token_loss: 0.62
decoder_out_loss: 1.03, postnet_out_loss: 2.50, stop_token_loss: 0.61
decoder_out_loss: 1.07, postnet_out_loss: 3.19, stop_token_loss: 0.62
decoder_out_loss: 0.99, postnet_out_loss: 2.44, stop_token_loss: 0.58
decoder_out_loss: 1.07, postnet_out_loss: 2.64, stop_token_loss: 0.60
decoder_out_loss: 1.02, postnet_out_loss: 2.99, stop_token_loss: 0.64
decoder_out_loss: 1.03, postnet_out_loss: 3.68, stop_token_loss: 0.61
decoder_out_loss: 1.05, postnet_out_loss: 2.36, stop_token_loss: 0.56
decoder_out_loss: 1.04, postnet_out_loss: 3.23, stop_token_loss: 0.57
decoder_out_loss: 1.06, postnet_out_loss: 2.52, stop_token_loss: 0.58
decoder_out_loss: 1.12, postnet_out_loss: 2.25, stop_token_loss: 0.52
decoder_out_loss: 1.00, postnet_out_loss: 2.16, stop_token_loss: 0.55

アテンション重みの可視化

ここでは、学習が正常に進行していない場合の例として、意図的に学習済みモデルの一部のパラメータを乱数で初期化します。詳細は、randomize_tts_engine_ 参照してください。

[40]:
from ttslearn.tacotron import Tacotron2TTS
from ttslearn.tacotron.tts import randomize_tts_engine_

tacotron_engine = Tacotron2TTS()

tacotron_engine_bad = Tacotron2TTS()
randomize_tts_engine_(tacotron_engine_bad)
print("randomized some of network weights")
randomized some of network weights
[41]:
text = "水をマレーシアから買わなくてはならないのです。"

import pyopenjtalk
from ttslearn.tacotron.frontend.openjtalk import text_to_sequence, pp_symbols

labels = pyopenjtalk.extract_fullcontext(text)
# 韻律記号付き音素列
in_feats = text_to_sequence(pp_symbols(labels))
in_feats = torch.tensor(in_feats, dtype=torch.long)

with torch.no_grad():
    outs, outs_fine, logits, att_ws = tacotron_engine.acoustic_model.inference(in_feats)

with torch.no_grad():
    outs2, outs_fine2, logits2, att_ws2 = tacotron_engine_bad.acoustic_model.inference(in_feats)
[42]:
fig, ax = plt.subplots(1, 2, figsize=(12, 5))
ax[0].set_title("Failure")
ax[1].set_title("Normal")

mesh = ax[0].imshow(att_ws2.cpu().data.numpy().T, aspect="auto", origin="lower", interpolation="nearest")
fig.colorbar(mesh, ax=ax[0])
ax[0].set_xlabel("Decoder time step [frame]")
ax[0].set_ylabel("Encoder time step [phoneme]")

mesh = ax[1].imshow(att_ws.cpu().data.numpy().T, aspect="auto", origin="lower", interpolation="nearest")
fig.colorbar(mesh, ax=ax[1])
ax[1].set_xlabel("Decoder time step [frame]")
ax[1].set_ylabel("Encoder time step [phoneme]")

plt.tight_layout()

# 図10-5
savefig("./fig/e2etts_impl_attention_failure")
../_images/notebooks_ch10_Recipe-Tacotron_77_0.png

実用的な学習スクリプトの実装

train_tacotron.py を参照してください。

10.5 Tacotron の学習

Tacotron の設定ファイル

[43]:
! cat conf/train_tacotron/model/{acoustic_config_name}.yaml
netG:
  _target_: ttslearn.tacotron.Tacotron2
  num_vocab: 52
  reduction_factor: 2
  embed_dim: 512
  encoder_hidden_dim: 512
  decoder_out_dim: 80
  encoder_conv_layers: 3
  encoder_conv_channels: 512
  encoder_conv_kernel_size: 5
  encoder_dropout: 0.5
  attention_hidden_dim: 128
  attention_conv_channels: 32
  attention_conv_kernel_size: 31
  decoder_layers: 2
  decoder_hidden_dim: 1024
  decoder_prenet_layers: 2
  decoder_prenet_hidden_dim: 256
  decoder_prenet_dropout: 0.5
  postnet_layers: 5
  postnet_channels: 512
  postnet_kernel_size: 5
  postnet_dropout: 0.5
  decoder_zoneout: 0.1

Tacotron のインスタンス化

[44]:
import hydra
from omegaconf import OmegaConf
hydra.utils.instantiate(OmegaConf.load(f"./conf/train_tacotron/model/{acoustic_config_name}.yaml")["netG"])
[44]:
Tacotron2(
  (encoder): Encoder(
    (embed): Embedding(52, 512, padding_idx=0)
    (convs): Sequential(
      (0): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (1): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (2): ReLU()
      (3): Dropout(p=0.5, inplace=False)
      (4): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (5): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (6): ReLU()
      (7): Dropout(p=0.5, inplace=False)
      (8): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (9): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (10): ReLU()
      (11): Dropout(p=0.5, inplace=False)
    )
    (blstm): LSTM(512, 256, batch_first=True, bidirectional=True)
  )
  (decoder): Decoder(
    (attention): LocationSensitiveAttention(
      (mlp_enc): Linear(in_features=512, out_features=128, bias=True)
      (mlp_dec): Linear(in_features=1024, out_features=128, bias=False)
      (mlp_att): Linear(in_features=32, out_features=128, bias=False)
      (loc_conv): Conv1d(1, 32, kernel_size=(31,), stride=(1,), padding=(15,), bias=False)
      (w): Linear(in_features=128, out_features=1, bias=True)
    )
    (prenet): Prenet(
      (prenet): Sequential(
        (0): Linear(in_features=80, out_features=256, bias=True)
        (1): ReLU()
        (2): Linear(in_features=256, out_features=256, bias=True)
        (3): ReLU()
      )
    )
    (lstm): ModuleList(
      (0): ZoneOutCell(
        (cell): LSTMCell(768, 1024)
      )
      (1): ZoneOutCell(
        (cell): LSTMCell(1024, 1024)
      )
    )
    (feat_out): Linear(in_features=1536, out_features=160, bias=False)
    (prob_out): Linear(in_features=1536, out_features=2, bias=True)
  )
  (postnet): Postnet(
    (postnet): Sequential(
      (0): Conv1d(80, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (1): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (2): Tanh()
      (3): Dropout(p=0.5, inplace=False)
      (4): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (5): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (6): Tanh()
      (7): Dropout(p=0.5, inplace=False)
      (8): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (9): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (10): Tanh()
      (11): Dropout(p=0.5, inplace=False)
      (12): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (13): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (14): Tanh()
      (15): Dropout(p=0.5, inplace=False)
      (16): Conv1d(512, 80, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (17): BatchNorm1d(80, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (18): Dropout(p=0.5, inplace=False)
    )
  )
)

レシピの stage 3 の実行

[45]:
if run_sh:
    ! ./run.sh --stage 3 --stop-stage 3 --acoustic-model $acoustic_config_name \
        --tqdm $run_sh_tqdm --tacotron-train-max-train-steps $tacotron_max_train_steps \
        --tacotron-data-batch-size $tacotron_batch_size \
        --cudnn-benchmark $cudnn_benchmark --cudnn-deterministic $cudnn_deterministic

損失関数の値の推移

著者による実験結果です。Tensorboardのログは https://tensorboard.dev/ にアップロードされています。 ログデータをtensorboard パッケージを利用してダウンロードします。

https://tensorboard.dev/experiment/yXyg9qgfQRSGxvil5FA4xw/

[46]:
if exists("tensorboard/all_log.csv"):
    df = pd.read_csv("tensorboard/all_log.csv")
else:
    experiment_id = "gHKogn7wRxa4B3NIVw27xw"
    experiment = tb.data.experimental.ExperimentFromDev(experiment_id)
    df = experiment.get_scalars()
    df.to_csv("tensorboard/all_log.csv", index=False)
df["run"].unique()
[46]:
array(['jsut_sr16000_tacotron2_rf2',
       'jsut_sr16000_wavenet_sr16k_mulaw256_30layers'], dtype=object)
[47]:
tacotron_loss = df[df.run.str.contains("tacotron2_rf2")]

tacotron_train_loss = tacotron_loss[tacotron_loss.tag.str.startswith("Loss/train")]
tacotron_dev_loss = tacotron_loss[tacotron_loss.tag.str.startswith("Loss/dev")]

fig, ax = plt.subplots(figsize=(6,4))
ax.plot(tacotron_train_loss["step"], tacotron_train_loss["value"], label="Train")
ax.plot(tacotron_dev_loss["step"], tacotron_dev_loss["value"], "--", label="Dev")
ax.set_xlabel("Epoch")
ax.set_ylabel("Epoch loss")
plt.legend()

# 図10-6
savefig("fig/tacotron_impl_tacotron_loss")
../_images/notebooks_ch10_Recipe-Tacotron_89_0.png

10.6 WaveNet ボコーダ学習

WaveNetボコーダ の設定ファイル

[48]:
! cat conf/train_wavenet/model/{wavenet_config_name}.yaml
netG:
  _target_: ttslearn.wavenet.WaveNet
  out_channels: 256
  layers: 30
  stacks: 3
  residual_channels: 64
  gate_channels: 128
  skip_out_channels: 64
  kernel_size: 3
  cin_channels: 80
  upsample_scales: [2, 4, 5, 5] # np.prod(upsample_scales) = 200
  aux_context_window: 2

WaveNetボコーダ のインスタンス化

[49]:
import hydra
from omegaconf import OmegaConf
# WaveNet の 30層 すべてを表示すると長くなるため、ここでは省略します。
# hydra.utils.instantiate(OmegaConf.load(f"./conf/train_wavenet/model/{wavenet_config_name}.yaml")["netG"])

レシピの stage 4 の実行

[50]:
if run_sh:
    ! ./run.sh --stage 4 --stop-stage 4 --wavenet-model $wavenet_config_name \
        --tqdm $run_sh_tqdm --wavenet-train-max-train-steps $wavenet_max_train_steps \
        --wavenet-data-batch-size $wavenet_batch_size \
        --cudnn-benchmark $cudnn_benchmark --cudnn-deterministic $cudnn_deterministic

損失関数の値の推移

[51]:
wavenet_loss = df[df.run.str.contains("wavenet")]

wavenet_train_loss = wavenet_loss[wavenet_loss.tag.str.contains("Loss/train")]
wavenet_dev_loss = wavenet_loss[wavenet_loss.tag.str.contains("Loss/dev")]

fig, ax = plt.subplots(figsize=(6,4))
ax.plot(wavenet_train_loss["step"], wavenet_train_loss["value"], label="Train")
ax.plot(wavenet_dev_loss["step"], wavenet_dev_loss["value"], "--", label="Dev")
ax.set_xlabel("Epoch")
ax.set_ylabel("Epoch loss")
ax.set_ylim(1.6, 2.3)
plt.legend()

# 図10-7
savefig("fig/tacotron_impl_wavenet_loss")
../_images/notebooks_ch10_Recipe-Tacotron_98_0.png

10.7 学習済みモデルを用いてテキストから音声を合成

学習済みモデルの読み込み

[52]:
import joblib
device = torch.device("cpu")

Tacotronの読み込み

[53]:
acoustic_config = OmegaConf.load(f"exp/jsut_sr16000/{acoustic_config_name}/model.yaml")
acoustic_model = hydra.utils.instantiate(acoustic_config.netG)
checkpoint = torch.load(f"exp/jsut_sr16000/{acoustic_config_name}/latest.pth", map_location=device)
acoustic_model.load_state_dict(checkpoint["state_dict"])
acoustic_model.eval();
[53]:
Tacotron2(
  (encoder): Encoder(
    (embed): Embedding(52, 512, padding_idx=0)
    (convs): Sequential(
      (0): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (1): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (2): ReLU()
      (3): Dropout(p=0.5, inplace=False)
      (4): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (5): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (6): ReLU()
      (7): Dropout(p=0.5, inplace=False)
      (8): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (9): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (10): ReLU()
      (11): Dropout(p=0.5, inplace=False)
    )
    (blstm): LSTM(512, 256, batch_first=True, bidirectional=True)
  )
  (decoder): Decoder(
    (attention): LocationSensitiveAttention(
      (mlp_enc): Linear(in_features=512, out_features=128, bias=True)
      (mlp_dec): Linear(in_features=1024, out_features=128, bias=False)
      (mlp_att): Linear(in_features=32, out_features=128, bias=False)
      (loc_conv): Conv1d(1, 32, kernel_size=(31,), stride=(1,), padding=(15,), bias=False)
      (w): Linear(in_features=128, out_features=1, bias=True)
    )
    (prenet): Prenet(
      (prenet): Sequential(
        (0): Linear(in_features=80, out_features=256, bias=True)
        (1): ReLU()
        (2): Linear(in_features=256, out_features=256, bias=True)
        (3): ReLU()
      )
    )
    (lstm): ModuleList(
      (0): ZoneOutCell(
        (cell): LSTMCell(768, 1024)
      )
      (1): ZoneOutCell(
        (cell): LSTMCell(1024, 1024)
      )
    )
    (feat_out): Linear(in_features=1536, out_features=160, bias=False)
    (prob_out): Linear(in_features=1536, out_features=2, bias=True)
  )
  (postnet): Postnet(
    (postnet): Sequential(
      (0): Conv1d(80, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (1): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (2): Tanh()
      (3): Dropout(p=0.5, inplace=False)
      (4): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (5): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (6): Tanh()
      (7): Dropout(p=0.5, inplace=False)
      (8): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (9): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (10): Tanh()
      (11): Dropout(p=0.5, inplace=False)
      (12): Conv1d(512, 512, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (13): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (14): Tanh()
      (15): Dropout(p=0.5, inplace=False)
      (16): Conv1d(512, 80, kernel_size=(5,), stride=(1,), padding=(2,), bias=False)
      (17): BatchNorm1d(80, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (18): Dropout(p=0.5, inplace=False)
    )
  )
)

WaveNetボコーダの読み込み

[54]:
wavenet_config = OmegaConf.load(f"exp/jsut_sr16000/{wavenet_config_name}/model.yaml")
wavenet_model = hydra.utils.instantiate(wavenet_config.netG)
checkpoint = torch.load(f"exp/jsut_sr16000/{wavenet_config_name}/latest_ema.pth", map_location=device)
wavenet_model.load_state_dict(checkpoint["state_dict"])
# weight normalization は推論時には不要なため除く
wavenet_model.remove_weight_norm_()
wavenet_model.eval();
[54]:
WaveNet(
  (first_conv): Conv1d(256, 64, kernel_size=(1,), stride=(1,))
  (main_conv_layers): ModuleList(
    (0): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(2,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (1): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(4,), dilation=(2,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (2): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(8,), dilation=(4,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (3): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(16,), dilation=(8,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (4): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(32,), dilation=(16,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (5): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(64,), dilation=(32,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (6): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(128,), dilation=(64,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (7): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(256,), dilation=(128,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (8): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(512,), dilation=(256,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (9): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(1024,), dilation=(512,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (10): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(2,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (11): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(4,), dilation=(2,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (12): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(8,), dilation=(4,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (13): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(16,), dilation=(8,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (14): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(32,), dilation=(16,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (15): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(64,), dilation=(32,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (16): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(128,), dilation=(64,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (17): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(256,), dilation=(128,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (18): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(512,), dilation=(256,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (19): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(1024,), dilation=(512,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (20): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(2,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (21): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(4,), dilation=(2,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (22): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(8,), dilation=(4,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (23): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(16,), dilation=(8,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (24): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(32,), dilation=(16,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (25): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(64,), dilation=(32,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (26): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(128,), dilation=(64,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (27): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(256,), dilation=(128,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (28): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(512,), dilation=(256,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
    (29): ResSkipBlock(
      (conv): Conv1d(64, 128, kernel_size=(3,), stride=(1,), padding=(1024,), dilation=(512,))
      (conv1x1c): Conv1d(80, 128, kernel_size=(1,), stride=(1,), bias=False)
      (conv1x1_out): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
      (conv1x1_skip): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    )
  )
  (last_conv_layers): ModuleList(
    (0): ReLU()
    (1): Conv1d(64, 64, kernel_size=(1,), stride=(1,))
    (2): ReLU()
    (3): Conv1d(64, 256, kernel_size=(1,), stride=(1,))
  )
  (upsample_net): ConvInUpsampleNetwork(
    (conv_in): Conv1d(80, 80, kernel_size=(5,), stride=(1,), bias=False)
    (upsample): UpsampleNetwork(
      (conv_layers): ModuleList(
        (0): Conv2d(1, 1, kernel_size=(1, 5), stride=(1, 1), padding=(0, 2), bias=False)
        (1): Conv2d(1, 1, kernel_size=(1, 9), stride=(1, 1), padding=(0, 4), bias=False)
        (2): Conv2d(1, 1, kernel_size=(1, 11), stride=(1, 1), padding=(0, 5), bias=False)
        (3): Conv2d(1, 1, kernel_size=(1, 11), stride=(1, 1), padding=(0, 5), bias=False)
      )
    )
  )
)

統計量の読み込み

統計量は、Griffin-Limのアルゴリズムを利用する場合にのみ必要となります。

[55]:
acoustic_out_scaler = joblib.load("./dump/jsut_sr16000/norm/out_tacotron_scaler.joblib")

メルスペクトログラムの予測

[56]:
from ttslearn.util import find_lab, find_feats

labels = hts.load(find_lab("downloads/jsut-label/", test_utt))

in_feats = text_to_sequence(pp_symbols(labels.contexts))
in_feats = torch.tensor(in_feats, dtype=torch.long).to(device)

with torch.no_grad():
    out_feats, out_feats_fine, stop_flags, alignment = acoustic_model.inference(in_feats)

# 比較用に、自然音声から抽出された音響特徴量を読み込みむ
feats = np.load(find_feats("dump/jsut_sr16000/norm/", test_utt, typ="out_tacotron"))

メルスペクトログラムの可視化

[57]:
fig, ax = plt.subplots(2, 1, figsize=(8,6))
ax[0].set_title("Mel-spectrogram of natural speech")
ax[1].set_title("Mel-spectrogram of Tacotron output")

mindb = min(feats.min(), out_feats_fine.min())
maxdb = max(feats.max(), out_feats_fine.max())

hop_length = int(sr * 0.0125)
mesh = librosa.display.specshow(
    feats.T, sr=sr, x_axis="time", y_axis="frames", hop_length=hop_length, cmap=cmap, ax=ax[0])
mesh.set_clim(mindb, maxdb)
fig.colorbar(mesh, ax=ax[0])
mesh = librosa.display.specshow(
    out_feats_fine.data.numpy().T, sr=sr, x_axis="time", y_axis="frames", hop_length=hop_length, cmap=cmap, ax=ax[1])
mesh.set_clim(mindb, maxdb)
fig.colorbar(mesh, ax=ax[1])

for a in ax:
    a.set_xlabel("Time [sec]")
    a.set_ylabel("Mel filter channel")
fig.tight_layout()

# 図10-8
savefig("./fig/e2etts_impl_logmel_comp")
../_images/notebooks_ch10_Recipe-Tacotron_111_0.png

アテンション重みの可視化

[58]:
fig, ax = plt.subplots(figsize=(6,4))
im = ax.imshow(alignment.cpu().data.numpy().T, aspect="auto", origin="lower", interpolation="nearest")
fig.colorbar(im, ax=ax)
ax.set_xlabel("Decoder time step [frame]")
ax.set_ylabel("Encoder time step [phoneme]");
[58]:
Text(0, 0.5, 'Encoder time step [phoneme]')
../_images/notebooks_ch10_Recipe-Tacotron_113_1.png

Stop token の可視化

[59]:
fig, ax = plt.subplots(figsize=(6,4))
ax.plot(torch.sigmoid(stop_flags).cpu().numpy())
ax.set_xlabel("Time [frame]")
ax.set_ylabel("Stop probability");
[59]:
Text(0, 0.5, 'Stop probability')
../_images/notebooks_ch10_Recipe-Tacotron_115_1.png

音声波形の生成

[60]:
from ttslearn.dsp import inv_mulaw_quantize

@torch.no_grad()
def gen_waveform(wavenet_model, out_feats):
    # (B, T, C) -> (B, C, T)
    c = out_feats.view(1, -1, out_feats.size(-1)).transpose(1, 2)

    # 音声のサンプル数を計算
    upsample_scale = np.prod(wavenet_model.upsample_scales)
    T = (
        c.shape[-1] - wavenet_model.aux_context_window * 2
    ) * upsample_scale

    # WaveNet による音声波形の生成
    # NOTE: 計算に時間がかかるため、tqdm によるプログレスバーを受け付けるようにしています
    gen_wav = wavenet_model.inference(c, T, tqdm)

    # One-hot ベクトルから1次元の信号に変換
    gen_wav = gen_wav.max(1)[1].float().cpu().numpy().reshape(-1)

    # Mu-law 量子化の逆変換
    gen_wav = inv_mulaw_quantize(
        gen_wav, wavenet_model.out_channels - 1
    )

    return gen_wav

すべてのモデルを組み合わせて音声波形の生成

[61]:
from ttslearn.util import find_lab, find_feats
from ttslearn.dsp import logmelspectrogram_to_audio

# WaveNetボコーダの代わりにGriffin-Limのアルゴリズムを利用する場合、以下をTrueにしてください。
griffin_lim = False

labels = hts.load(find_lab("downloads/jsut-label/", test_utt))
in_feats = text_to_sequence(pp_symbols(labels.contexts))
in_feats = torch.tensor(in_feats, dtype=torch.long).to(device)

with torch.no_grad():
    _, out_feats, _, _ = acoustic_model.inference(in_feats)

if griffin_lim:
    # Griffin-Lim のアルゴリズムに基づく音声波形生成
    out_feats = out_feats.cpu().data.numpy()
    # 正規化の逆変換
    logmel = acoustic_out_scaler.inverse_transform(out_feats)
    gen_wav = logmelspectrogram_to_audio(logmel, sr)
else:
    # WaveNet ボコーダによる音声波形の生成
    gen_wav = gen_waveform(wavenet_model, out_feats)
[62]:
# 比較用に元音声の読み込み
from scipy.io import wavfile
_sr, ref_wav = wavfile.read(f"./downloads/jsut_ver1.1/basic5000/wav/{test_utt}.wav")
ref_wav = (ref_wav / 32768.0).astype(np.float64)
ref_wav = librosa.resample(ref_wav, _sr, sr)
[63]:
fig, ax = plt.subplots(2, 1, figsize=(8,6))

hop_length = int(sr * 0.005)
fft_size = pyworld.get_cheaptrick_fft_size(sr)

# Tacotronの出力と粗く揃えるために、自然音声の冒頭と末尾の無音区間を削除
ref_wav_trim = librosa.effects.trim(ref_wav, top_db=20)[0]

spec_ref = librosa.stft(ref_wav_trim, n_fft=fft_size, hop_length=hop_length, window="hann")
logspec_ref = np.log(np.abs(spec_ref))
spec_gen = librosa.stft(gen_wav, n_fft=fft_size, hop_length=hop_length, window="hann")
logspec_gen = np.log(np.abs(spec_gen))

mindb = min(logspec_ref.min(), logspec_gen.min())
maxdb = max(logspec_ref.max(), logspec_gen.max())

mesh = librosa.display.specshow(logspec_ref, hop_length=hop_length, sr=sr, cmap=cmap, x_axis="time", y_axis="hz", ax=ax[0])
mesh.set_clim(mindb, maxdb)
fig.colorbar(mesh, ax=ax[0], format="%+2.fdB")

mesh = librosa.display.specshow(logspec_gen, hop_length=hop_length, sr=sr, cmap=cmap, x_axis="time", y_axis="hz", ax=ax[1])
mesh.set_clim(mindb, maxdb)
fig.colorbar(mesh, ax=ax[1], format="%+2.fdB")

ax[0].set_title("Spectrogram of natural speech")
ax[1].set_title("Spectrogram of generated speech")

for a in ax:
    a.set_xlabel("Time [sec]")
    a.set_ylabel("Frequency [Hz]")

plt.tight_layout()

print("自然音声")
IPython.display.display(Audio(ref_wav_trim, rate=sr))
print("Tacotron 2による合成音声")
IPython.display.display(Audio(gen_wav, rate=sr))

# 図10-9
savefig("./fig/e2etts_impl_tts_spec_comp")
自然音声
Tacotron 2による合成音声
../_images/notebooks_ch10_Recipe-Tacotron_121_4.png

合成音声のより詳細な比較 (bonus)

[64]:
# 比較用に、自然音声から抽出されたメルスペクトログラムから音声波形の生成を行います
feats = np.load(find_feats("dump/jsut_sr16000/norm/", test_utt, typ="out_tacotron"))
feats = torch.from_numpy(feats)
gen_wav_wn_gt = gen_waveform(wavenet_model, feats)
[65]:
ref_wav_inv = np.load(find_feats("./dump/jsut_sr16000/org/", test_utt, typ="out_wavenet"))
ref_wav_inv = inv_mulaw_quantize(ref_wav_inv, 255)
[66]:
print("自然音声")
IPython.display.display(Audio(ref_wav, rate=sr))
print("自然音声 (8-bit mu-law)")
IPython.display.display(Audio(ref_wav_inv, rate=sr))
print("WaveNetボコーダの出力")
IPython.display.display(Audio(gen_wav_wn_gt, rate=sr))
print("Tacotron + WaveNetボコーダの出力")
IPython.display.display(Audio(gen_wav, rate=sr))
自然音声
自然音声 (8-bit mu-law)
WaveNetボコーダの出力
Tacotron + WaveNetボコーダの出力

評価データに対して音声波形生成

レシピの stage 5 の実行

[67]:
if run_sh:
    ! ./run.sh --stage 5 --stop-stage 5 --acoustic-model $acoustic_config_name \
        --tqdm $run_sh_tqdm --wavenet-model $wavenet_config_name \
        --reverse true --num-eval-utts $num_eval_utts

レシピの stage 6 の実行

[68]:
if run_sh and run_stage6:
    ! ./run.sh --stage 6 --stop-stage 6 --acoustic-model $acoustic_config_name \
        --tqdm $run_sh_tqdm --wavenet-model $wavenet_config_name \
        --reverse true --num-eval-utts $num_eval_utts

自然音声と合成音声の比較 (bonus)

[69]:
from pathlib import Path
from ttslearn.util import load_utt_list

with open("./downloads/jsut_ver1.1/basic5000/transcript_utf8.txt") as f:
    transcripts = {}
    for l in f:
        utt_id, script = l.split(":")
        transcripts[utt_id] = script

eval_list = load_utt_list("data/eval.list")[::-1][:5]

for utt_id in eval_list:
    # ref file
    ref_file = f"./downloads/jsut_ver1.1/basic5000/wav/{utt_id}.wav"
    _sr, ref_wav = wavfile.read(ref_file)
    ref_wav = (ref_wav / 32768.0).astype(np.float64)
    ref_wav = librosa.resample(ref_wav, _sr, sr)

    print(f"{utt_id}: {transcripts[utt_id]}")
    print("自然音声")
    IPython.display.display(Audio(ref_wav, rate=sr))

    gen_file = f"exp/jsut_sr16000/synthesis_{acoustic_config_name}_griffin_lim/eval/{utt_id}.wav"
    if exists(gen_file):
        _sr, gen_wav = wavfile.read(gen_file)
        print("Tacotron + Griffin-Lim")
        IPython.display.display(Audio(gen_wav, rate=sr))
    else:
        print("Tacotron + Griffin-Lim: not found")

    gen_file_wn = f"exp/jsut_sr16000/synthesis_{acoustic_config_name}_{wavenet_config_name}/eval/{utt_id}.wav"
    if exists(gen_file_wn):
        _sr, gen_wav_wn = wavfile.read(gen_file_wn)
        print("Tacotron + WaveNetボコーダ")
        IPython.display.display(Audio(gen_wav_wn, rate=sr))
    else:
        print("Tacotron + WaveNetボコーダ: not found")
BASIC5000_5000: あと30分の猶予が与えられた。

自然音声
Tacotron + Griffin-Lim
Tacotron + WaveNetボコーダ
BASIC5000_4999: ドナーから腎臓の提供を受ける。

自然音声
Tacotron + Griffin-Lim
Tacotron + WaveNetボコーダ
BASIC5000_4998: 村人たちは、その話を聞いて、震え上がった。

自然音声
Tacotron + Griffin-Lim
Tacotron + WaveNetボコーダ
BASIC5000_4997: その王国は、最後の王に嗣子がおらず、滅亡した。

自然音声
Tacotron + Griffin-Lim
Tacotron + WaveNetボコーダ
BASIC5000_4996: 扇型の、弧の長さを、計算で求める。

自然音声
Tacotron + Griffin-Lim
Tacotron + WaveNetボコーダ

学習済みモデルのパッケージング (bonus)

学習済みモデルを利用したTTSに必要なファイルをすべて単一のディレクトリにまとめます。 ttslearn.tacotron.Tacotron2TTS クラスには、まとめたディレクトリを指定し、TTSを行う機能が実装されています。

レシピの stage 99 の実行

[70]:
if run_sh:
    ! ./run.sh --stage 99 --stop-stage 99 --acoustic-model $acoustic_config_name \
        --wavenet-model $wavenet_config_name
[71]:
!ls tts_models/jsut_sr16000_{acoustic_config_name}_{wavenet_config_name}
acoustic_model.pth   out_tacotron_scaler_mean.npy   wavenet_model.pth
acoustic_model.yaml  out_tacotron_scaler_scale.npy  wavenet_model.yaml
config.yaml          out_tacotron_scaler_var.npy

パッケージングしたモデルを利用したTTS

[72]:
from ttslearn.tacotron import Tacotron2TTS

# パッケージングしたモデルのパスを指定します
engine = Tacotron2TTS(
    model_dir=f"./tts_models/jsut_sr16000_{acoustic_config_name}_{wavenet_config_name}"
)
wav, sr = engine.tts("ここまでお読みいただき、ありがとうございました。", tqdm=tqdm)

fig, ax = plt.subplots(figsize=(8,2))
librosa.display.waveplot(wav.astype(np.float32), sr, ax=ax)
ax.set_xlabel("Time [sec]")
ax.set_ylabel("Amplitude")
plt.tight_layout()

Audio(wav, rate=sr)
[72]:
../_images/notebooks_ch10_Recipe-Tacotron_138_2.png
[73]:
if is_colab():
    from datetime import timedelta
    elapsed = (time.time() - start_time)
    print("所要時間:", str(timedelta(seconds=elapsed)))