第7章 WaveNet: 深層学習に基づく音声波形の生成モデル

Open In Colab

準備

Python version

[1]:
!python -VV
Python 3.9.1 (default, Feb  5 2021, 16:06:04)
[GCC 9.3.0]

ttslearn のインストール

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

パッケージのインポート

[4]:
%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
[5]:
# 数値演算
import numpy as np
import torch
from torch import nn
# 音声波形の読み込み
from scipy.io import wavfile
# 音声分析、可視化
import librosa
import librosa.display
# Pythonで学ぶ音声合成
import ttslearn
[6]:
# シードの固定
from ttslearn.util import init_seed
init_seed(773)
[7]:
torch.__version__
[7]:
'1.9.0'

描画周りの設定

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

7.3 WaveNetにおける音声波形の扱い

\(\mu\)-law アルゴリズム

[9]:
def mulaw(x, mu=255):
    return np.sign(x) * np.log1p(mu * np.abs(x)) / np.log1p(mu)

def quantize(y, mu=255, offset=1):
    # [-1, 1] -> [0, 2] -> [0, 1] -> [0, mu]
    return ((y + offset) / 2 * mu).astype(np.int64)

def mulaw_quantize(x, mu=255):
    return quantize(mulaw(x, mu), mu)

\(\mu\)-law アルゴリズム適用前

[10]:
sr, x = wavfile.read(ttslearn.util.example_audio_file())
x = (x / 32768.0).astype(np.float32)

mu = 2**8-1 # 8-bit

fig, ax = plt.subplots(2, 1, figsize=(6,4))
ax[0].set_title("Waveform")
ax[1].set_title("Histrogram")

ax[0].set_ylim(-0.9, 0.9)
librosa.display.waveplot(x, ax=ax[0], sr=16000)

ax[1].set_xlim(-0.9, 0.9)
ax[1].hist(x, bins=mu)

ax[0].set_xlabel("Time [sec]")
ax[0].set_ylabel("Amplitude")
ax[1].set_xlabel("Amplitude")
ax[1].set_ylabel("Count")

plt.tight_layout()

# 図7-6 (a)
savefig("./fig/wavenet_mulaw_a")
../_images/notebooks_ch07_WaveNet_18_0.png

\(\mu\)-law アルゴリズム適用後

[11]:
fig, ax = plt.subplots(2, 1, figsize=(6,4))
ax[0].set_title("Waveform")
ax[1].set_title("Histrogram")

ax[0].set_ylim(-0.9, 0.9)
librosa.display.waveplot(mulaw(x), ax=ax[0], sr=16000)

ax[1].set_xlim(-0.9, 0.9)
ax[1].hist(mulaw(x), bins=mu)

ax[0].set_xlabel("Time [sec]")
ax[0].set_ylabel("Amplitude")
ax[1].set_xlabel("Amplitude")
ax[1].set_ylabel("Count")

plt.tight_layout()

# 図7-6 (b)
savefig("./fig/wavenet_mulaw_b")
../_images/notebooks_ch07_WaveNet_20_0.png

\(\mu\)-law アルゴリズムによる逆変換

[12]:
def inv_mulaw(y, mu=255):
    return np.sign(y) * (1.0 / mu) * ((1.0 + mu)**np.abs(y) - 1.0)

def inv_quantize(y, mu):
    # [0, mu] -> [-1, 1]
    return 2 * y.astype(np.float32) / mu - 1

def inv_mulaw_quantize(y, mu=255):
    return inv_mulaw(inv_quantize(y, mu), mu)

\(\mu\)-law なし

[13]:
sr, x = wavfile.read(ttslearn.util.example_audio_file())
x = (x / 32768.0).astype(np.float32)
x = librosa.resample(x, sr, 16000)
sr = 16000

bits = [8, 4]

fig, ax = plt.subplots(len(bits)+1, 1, figsize=(6,2*(len(bits)+1)), sharey=True)
ax[0].set_title("Input waveform")
librosa.display.waveplot(x, sr, x_axis="time", ax=ax[0])
IPython.display.display(Audio(x, rate=sr))

for idx, bit in enumerate(bits):
    mu = 2**bit - 1
    x_hat = inv_quantize(quantize(x, mu), mu)
    librosa.display.waveplot(x_hat, sr, x_axis="time", ax=ax[idx+1])
    ax[idx+1].set_title(f"{bit}-bit waveform")
    IPython.display.display(Audio(x_hat, rate=sr))

for a in ax:
    a.set_xlabel("Time [sec]")
    a.set_ylabel("Amplitude")
    a.set_xticks(np.arange(0, 3.5, 0.5))
    a.set_ylim(-0.5, 0.5)
plt.tight_layout()

# 図7-7 (a)
savefig("./fig/wavenet_inv_mulaw_waveform_a")
../_images/notebooks_ch07_WaveNet_24_3.png

\(\mu\)-law あり

[14]:
sr, x = wavfile.read(ttslearn.util.example_audio_file())
x = (x / 32768.0).astype(np.float32)
x = librosa.resample(x, sr, 16000)
sr = 16000

bits = [8, 4]

fig, ax = plt.subplots(len(bits)+1, 1, figsize=(6,2*(len(bits)+1)), sharey=True)
ax[0].set_title("Input waveform")
librosa.display.waveplot(x, sr, x_axis="time", ax=ax[0])
IPython.display.display(Audio(x, rate=sr))

for idx, bit in enumerate(bits):
    mu = 2**bit - 1
    x_hat = inv_mulaw_quantize(mulaw_quantize(x, mu), mu)
    librosa.display.waveplot(x_hat, sr, x_axis="time", ax=ax[idx+1])
    ax[idx+1].set_title(f"{bit}-bit waveform")
    IPython.display.display(Audio(x_hat, rate=sr))

for a in ax:
    a.set_xlabel("Time [sec]")
    a.set_ylabel("Amplitude")
    a.set_xticks(np.arange(0, 3.5, 0.5))
    a.set_ylim(-0.5, 0.5)
plt.tight_layout()

# 図7-7 (b)
savefig("./fig/wavenet_inv_mulaw_waveform_b")
../_images/notebooks_ch07_WaveNet_26_3.png

7.4 因果的な膨張畳み込み

1次元の畳み込み

[15]:
def _toy_1d_input():
    # (B, C, T) where B and C = 1
    return torch.tensor([1,2,3,0,1,2,4],dtype=torch.float).view(1,1,-1)

パディングを行わない場合

[16]:
conv = nn.Conv1d(1,1,3,bias=False, padding=0)
conv.weight.data[0,0,:] = torch.tensor([1,2,4],dtype=torch.float)

x = _toy_1d_input()
with torch.no_grad():
    y= conv(x)
print("入力:", x.long().view(-1).tolist())
print("出力:", y.long().view(-1).tolist())
入力: [1, 2, 3, 0, 1, 2, 4]
出力: [17, 8, 7, 10, 21]

パディングを行う場合

[17]:
conv = nn.Conv1d(1,1,3,bias=False, padding=1)
conv.weight.data[0,0,:] = torch.tensor([1,2,4],dtype=torch.float)

x = _toy_1d_input()
with torch.no_grad():
    y= conv(x)
print("入力:", x.long().view(-1).tolist())
print("出力:", y.long().view(-1).tolist())
入力: [1, 2, 3, 0, 1, 2, 4]
出力: [10, 17, 8, 7, 10, 21, 10]

2層の1次元畳み込み

[18]:
conv = nn.Conv1d(1,1,3,bias=False, padding=1)
conv.weight.data[0,0,:] = torch.tensor([1,2,4],dtype=torch.float)

x = _toy_1d_input()
with torch.no_grad():
    y= conv(conv(x))
print("入力:", x.long().view(-1).tolist())
print("出力:", y.long().view(-1).tolist())
入力: [1, 2, 3, 0, 1, 2, 4]
出力: [88, 76, 61, 62, 111, 92, 41]

因果的な畳み込み

[19]:
class CausalConv1d(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, **kwargs):
        super().__init__()
        self.padding = (kernel_size - 1)
        self.conv = nn.Conv1d(in_channels, out_channels, kernel_size, padding=self.padding, **kwargs)

    def forward(self, x):
        # 1 次元畳み込み
        y = self.conv(x)
        # 因果性を担保するために、順方向にシフトする
        if self.padding > 0:
            y = y[:, :, :-self.padding]
        return y
[20]:
conv = CausalConv1d(1,1,3,bias=False)
# テスト用に、畳み込みカーネルを手動で設定
conv.conv.weight.data[0,0,:] = torch.tensor([1,2,4],dtype=torch.float)

x = _toy_1d_input()
y= conv(x)
print("入力:", x.long().view(-1).tolist())
print("出力:", y.long().view(-1).tolist())
入力: [1, 2, 3, 0, 1, 2, 4]
出力: [4, 10, 17, 8, 7, 10, 21]

1次元膨張畳み込み

[21]:
class DilatedCausalConv1d(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, dilation=1, **kwargs):
        super().__init__()
        # パディングの幅を計算する際に、 dilation factor を考慮する必要があることに注意
        self.padding = (kernel_size - 1) * dilation
        self.conv = nn.Conv1d(in_channels, out_channels, kernel_size, padding=self.padding, dilation=dilation, **kwargs)

    def forward(self, x):
        # 1 次元畳み込み
        y = self.conv(x)
        # 因果性を担保するために、順方向にシフトする
        if self.padding > 0:
            y = y[:, :, :-self.padding]
        return y
[22]:
conv = DilatedCausalConv1d(1,1,3,dilation=2, bias=False)
# テスト用に、畳み込みカーネルを手動で設定
conv.conv.weight.data[0,0,:] = torch.tensor([1,2,4],dtype=torch.float)

x = _toy_1d_input()
y= conv(x)
print("入力:", x.long().view(-1).tolist())
print("出力:", y.long().view(-1).tolist())
入力: [1, 2, 3, 0, 1, 2, 4]
出力: [4, 8, 14, 4, 11, 10, 21]

7.5 ゲート付き活性化関数を用いた一次元畳み込み

[23]:
class GatedDilatedCausalConv1d(nn.Module):
    def __init__(self, in_channels, out_channels, kernel_size, dilation=1):
        super().__init__()
        self.padding = (kernel_size - 1) * dilation
        self.conv = nn.Conv1d(in_channels, out_channels*2, kernel_size, padding=self.padding, dilation=dilation)

    def forward(self, x):
        # 1 次元畳み込み
        y = self.conv(x)

        # 因果性を担保するために、順方向にシフトする
        if self.padding > 0:
            y = y[:, :, :-self.padding]

        # チャネル方向に分割
        a, b = y.split(y.size(1) // 2, dim=1)

        # ゲート付き活性化関数の適用
        y = torch.tanh(a) * torch.sigmoid(b)

        return y
[24]:
conv = GatedDilatedCausalConv1d(128, 16, 3, dilation=2)
x = torch.ones(32, 128, 100)
print("入力のサイズ:", tuple(x.shape))
print("出力のサイズ:", tuple(conv(x).shape))
入力のサイズ: (32, 128, 100)
出力のサイズ: (32, 16, 100)

7.6 条件付け特徴量のアップサンプリング

繰り返しに基づくアップサンプリング

[25]:
x = torch.tensor([[1, 2, 3],[1, 2, 3],[1,2,3]]).view(1,3,-1).float()
y = nn.Upsample(scale_factor=3, mode="nearest")(x)
print(x)
print(y)
tensor([[[1., 2., 3.],
         [1., 2., 3.],
         [1., 2., 3.]]])
tensor([[[1., 1., 1., 2., 2., 2., 3., 3., 3.],
         [1., 1., 1., 2., 2., 2., 3., 3., 3.],
         [1., 1., 1., 2., 2., 2., 3., 3., 3.]]])
[26]:
class RepeatUpsampling(nn.Module):
    def __init__(self, upsample_scales):
        super().__init__()
        self.upsample = nn.Upsample(scale_factor=np.prod(upsample_scales), mode="nearest")

    def forward(self, c):
        return self.upsample(c)
[27]:
c = torch.ones(32, 80, 10)
# 例として、100倍にアップサンプリング
c_up = RepeatUpsampling([100])(c)

print("入力のサイズ:", tuple(c.shape))
print("出力サイズ:", tuple(c_up.shape))
入力のサイズ: (32, 80, 10)
出力サイズ: (32, 80, 1000)

最近傍補間と畳み込みの併用に基づくアップサンプリング

[28]:
from torch.nn import functional as F

class UpsampleNetwork(nn.Module):
    def __init__(self, upsample_scales):
        super().__init__()
        self.upsample_scales = upsample_scales
        self.conv_layers = nn.ModuleList()
        for scale in upsample_scales:
            kernel_size = (1, scale * 2 + 1)
            conv = nn.Conv2d(
                1, 1, kernel_size=kernel_size, padding=(0, scale), bias=False
            )
            conv.weight.data.fill_(1.0 / np.prod(kernel_size))
            self.conv_layers.append(conv)

    def forward(self, c):
        # (B, 1, C, T)
        c = c.unsqueeze(1)
        # 最近傍補完と畳み込みの繰り返し
        for idx, scale in enumerate(self.upsample_scales):
            # 時間方向にのみアップサンプリング
            # (B, 1, C, T) -> (B, 1, C, T*scale)
            c = F.interpolate(c, scale_factor=(1, scale), mode="nearest")
            c = self.conv_layers[idx](c)
        # B x C x T
        return c.squeeze(1)
[29]:
c = torch.ones(32, 80, 10)
c_up = UpsampleNetwork([10, 8])(c)

print("入力のサイズ:", tuple(c.shape))
print("出力サイズ:", tuple(c_up.shape))
入力のサイズ: (32, 80, 10)
出力サイズ: (32, 80, 800)

実データ (mel-spectrogram) のアップサンプリング (bonus)

書籍では解説しませんでしたが、二次元畳み込みの重みを適切に初期化することで、畳み込みの前後でスケールが保持されることを示します。

[30]:
# 初期化の影響を確認するため、畳み込みのパラメータを乱数で初期化
class RandomInitUpsampleNetwork(UpsampleNetwork):
    def __init__(self, upsample_scales):
        super().__init__(upsample_scales)
        for conv in self.conv_layers:
            nn.init.normal_(conv.weight.data, 0, 1.0)
[31]:
from ttslearn.dsp import logmelspectrogram

_sr, x = wavfile.read(ttslearn.util.example_audio_file())
x = (x / 32768.0).astype(np.float32)
sr = 16000
x = librosa.resample(x, _sr, sr)
hop_length = int(0.0125 * sr)
sp = logmelspectrogram(x, sr, hop_length=hop_length)

fig, ax = plt.subplots(figsize=(8,4))
mesh = librosa.display.specshow(sp.T, sr=sr, hop_length=hop_length, cmap=cmap, x_axis="time", y_axis="frames")
fig.colorbar(mesh, ax=ax)
ax.set_xlabel("Time [sec]")
ax.set_ylabel("Frequency [Hz]")
plt.tight_layout()

Audio(x, rate=sr)
[31]: