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
論文のブロック図を再度掲載します。
(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はこれ”といった感じで動きます。
機械翻訳を例にすると、
①"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の規模が大きいのにも納得です・・。
まだつづきます