第7章 WaveNet: 深層学習に基づく音声波形の生成モデル¶
準備¶
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")

\(\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")

\(\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")

\(\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")

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]: