TACONTRON: A Fully End-to-End Text-To-Speech Synthesis Model
通常的TTS模型包含許多模塊载庭,例如文本分析看彼, 聲學模型, 音頻合成等囚聚。而構建這些模塊需要大量專業(yè)相關的知識以及特征工程靖榕,這將花費大量的時間和精力,而且各個模塊之間組合在一起也會產生很多新的問題顽铸。TACOTRON是一個端到端的深度學習TTS模型茁计,它可以說是將這些模塊都放在了一個黑箱子里,我們不用花費大量的時間去了解TTS中需要用的的模塊或者領域知識谓松,直接用深度學習的方法訓練出一個TTS模型星压,模型訓練完成后,給定input,模型就能生成對應的音頻鬼譬。
TACOTRON是一個端到端的TTS模型娜膘,模型核心是seq2seq + attention。模型的輸入為一系列文本字向量优质,輸出spectrogram frame, 然后在使用Griffin_lim算法生成對應音頻竣贪。模型結構如下圖:
模型結構:
CBHG模塊
encoder
decoder
post-processing net and waveform synthesis
Character Embedding
我們知道在訓練模型的時候,我們拿到的數(shù)據(jù)是一條長短不一的(text, audio)的數(shù)據(jù)盆赤,深度學習的核心其實就是大量的矩陣乘法贾富,對于模型而言,文本類型的數(shù)據(jù)是不被接受的牺六,所以這里我們需要先把文本轉化為對應的向量颤枪。這里涉及到如下幾個操作
構造字典
因為純文本數(shù)據(jù)是沒法作為深度學習輸入的,所以我們首先得把文本轉化為一個個對應的向量淑际,這里我使用字典下標作為字典中每一個字對應的id, 然后每一條文本就可以通過遍歷字典轉化成其對應的向量了畏纲。所以字典主要是應用在將文本轉化成其在字典中對應的id, 根據(jù)語料庫構造扇住,這里我使用的方法是根據(jù)語料庫中的字頻構造字典(我使用的是基于語料庫中的字構造字典,有的人可能會先分詞盗胀,基于詞構造艘蹋。不使用基于詞是現(xiàn)在就算是最好的分詞都會有一些誤分詞問題,而且基于字還可以在一定程度上緩解OOV的問題)票灰。下面是構造字典的代碼:
def create_vocabulary(vocabulary_path, data_paths, max_vocabulary_size, tokenizer=None):
? ? if not os.path.exists(vocabulary_path):
? ? ? ? print("Creating vocabulary %s from data %s" % (vocabulary_path, str(data_paths)))
? ? ? ? vocab = defaultdict(int)
? ? ? ? for path in data_paths:
? ? ? ? ? ? with codecs.open(path, mode="r", encoding="utf-8") as fr:
? ? ? ? ? ? ? ? counter = 0
? ? ? ? ? ? ? ? for line in fr:
? ? ? ? ? ? ? ? ? ? counter += 1
? ? ? ? ? ? ? ? ? ? if counter % 100000 == 0:
? ? ? ? ? ? ? ? ? ? ? ? print("? processing line %d" % (counter,))
? ? ? ? ? ? ? ? ? ? tokens = tokenizer(line)
? ? ? ? ? ? ? ? ? ? for w in tokens:
? ? ? ? ? ? ? ? ? ? ? ? word = re.sub(_DIGIT_RE, " ", w)
#? ? ? ? ? ? ? ? ? ? ? ? word = w
? ? ? ? ? ? ? ? ? ? ? ? vocab[word] += 1
? ? ? ? vocab_list = sorted(vocab, key=vocab.get, reverse=True)
? ? ? ? print("Vocabulary size: %d" % len(vocab_list))
? ? ? ? if len(vocab_list) > max_vocabulary_size:
? ? ? ? ? ? vocab_list = vocab_list[:max_vocabulary_size]
? ? ? ? with codecs.open(vocabulary_path, mode="w", encoding="utf-8") as vocab_file:
? ? ? ? ? ? for w in vocab_list:
? ? ? ? ? ? ? ? vocab_file.write(w + "\n")
然后我們就可以將文本數(shù)據(jù)轉化成對應的向量作為模型的輸入女阀。
embed layer
光有對應的id,沒法很好的表征文本信息屑迂,這里就涉及到構造詞向量浸策,關于詞向量不在說明,網(wǎng)上有很多資料惹盼,模型中使用詞嵌入層庸汗,通過訓練不斷的學習到語料庫中的每個字的詞向量,代碼如下:
def embed(inputs, vocab_size, num_units, zero_pad=True, scope="embedding", reuse=None):
? ? '''Embeds a given tensor.
? ? Args:
? ? ? inputs: A `Tensor` with type `int32` or `int64` containing the ids
? ? ? ? to be looked up in `lookup table`.
? ? ? vocab_size: An int. Vocabulary size.
? ? ? num_units: An int. Number of embedding hidden units.
? ? ? zero_pad: A boolean. If True, all the values of the fist row (id 0)
? ? ? ? should be constant zeros.
? ? ? scope: Optional scope for `variable_scope`.?
? ? ? reuse: Boolean, whether to reuse the weights of a previous layer
? ? ? ? by the same name.
? ? Returns:
? ? ? A `Tensor` with one more rank than inputs's. The last dimesionality
? ? ? ? should be `num_units`.
? ? '''
? ? with tf.variable_scope(scope, reuse=reuse):
? ? ? ? lookup_table = tf.get_variable('lookup_table',
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? dtype=tf.float32,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? shape=[vocab_size, num_units],
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? initializer=tf.truncated_normal_initializer(mean=0.0, stddev=0.01))
? ? ? ? if zero_pad:
? ? ? ? ? ? lookup_table = tf.concat((tf.zeros(shape=[1, num_units]),
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? lookup_table[1:, :]), 0)
? ? return tf.nn.embedding_lookup(lookup_table, inputs)?
值得注意的是手报,這里是隨機初始化詞嵌入層蚯舱,另一種方法是引入預先在語料庫訓練的詞向量(word2vec),可以在一定程度上提升模型的效果掩蛤。
音頻特征提取
對于音頻枉昏,我們主要是提取出它的melspectrogram音頻特征。MFCC是一種比較常用的音頻特征揍鸟,對于聲音來說凶掰,它其實是一個一維的時域信號,直觀上很難看出頻域的變化規(guī)律,我們知道,可以使用傅里葉變化询件,得到它的頻域信息桑阶,但是又丟失了時域信息,無法看到頻域隨時域的變化港华,這樣就沒法很好的描述聲音道川, 為了解決這個問題,很多時頻分析手段應運而生立宜。短時傅里葉冒萄,小波,Wigner分布等都是常用的時頻域分析方法橙数。這里我們使用短時傅里葉尊流。
所謂短時傅里葉變換,顧名思義灯帮,是對短時的信號做傅里葉變化崖技。那么短時的信號怎么得到的? 是長時的信號分幀得來的逻住。這么一想,STFT的原理非常簡單迎献,把一段長信號分幀(傅里葉變換適用于分析平穩(wěn)的信號瞎访。我們假設在較短的時間跨度范圍內,語音信號的變換是平坦的吁恍,這就是為什么要分幀的原因)扒秸、加窗,再對每一幀做傅里葉變換(FFT)冀瓦,最后把每一幀的結果沿另一個維度堆疊起來伴奥,得到類似于一幅圖的二維信號形式。如果我們原始信號是聲音信號咕幻,那么通過STFT展開得到的二維信號就是所謂的聲譜圖渔伯。
聲譜圖往往是很大的一張圖,為了得到合適大小的聲音特征肄程,往往把它通過梅爾標度濾波器組(mel-scale filter banks)锣吼,變換為梅爾頻譜。在梅爾頻譜上做倒譜分析(取對數(shù)蓝厌,做DCT變換)就得到了梅爾倒譜玄叠。音頻MFCC特征提取代碼如下,這里主要使用第三方庫librosa提取MFCC特征:
def get_spectrograms(sound_file):
? ? '''Extracts melspectrogram and log magnitude from given `sound_file`.
? ? Args:
? ? ? sound_file: A string. Full path of a sound file.
? ? Returns:
? ? ? Transposed S: A 2d array. A transposed melspectrogram with shape of (T, n_mels)
? ? ? Transposed magnitude: A 2d array.Has shape of (T, 1+hp.n_fft//2)
? ? '''
? ? # Loading sound file
? ? y, sr = librosa.load(sound_file, sr=hp.sr) # or set sr to hp.sr.
? ? # stft. D: (1+n_fft//2, T)
? ? D = librosa.stft(y=y,
? ? ? ? ? ? ? ? ? ? n_fft=hp.n_fft,
? ? ? ? ? ? ? ? ? ? hop_length=hp.hop_length,
? ? ? ? ? ? ? ? ? ? win_length=hp.win_length)
? ? # magnitude spectrogram
? ? magnitude = np.abs(D) #(1+n_fft/2, T)
? ? # power spectrogram
? ? power = magnitude**2 #(1+n_fft/2, T)
? ? # mel spectrogram
? ? S = librosa.feature.melspectrogram(S=power, n_mels=hp.n_mels) #(n_mels, T)
? ? return np.transpose(S.astype(np.float32)), np.transpose(magnitude.astype(np.float32)) # (T, n_mels), (T, 1+n_fft/2)
Encoder pre-net module
embeding layer之后是一個encoder pre-net模塊拓提,它有兩個隱藏層读恃,層與層之間的連接均是全連接;第一層的隱藏單元數(shù)目與輸入單元數(shù)目一致代态,第二層的隱藏單元數(shù)目為第一層的一半寺惫;兩個隱藏層采用的激活函數(shù)均為ReLu,并保持0.5的dropout來提高泛化能力
def prenet(inputs, is_training=True, scope="prenet", reuse=None):
? ? '''Prenet for Encoder and Decoder.
? ? Args:
? ? ? inputs: A 3D tensor of shape [N, T, hp.embed_size].
? ? ? is_training: A boolean.
? ? ? scope: Optional scope for `variable_scope`.?
? ? ? reuse: Boolean, whether to reuse the weights of a previous layer
? ? ? ? by the same name.
? ? Returns:
? ? ? A 3D tensor of shape [N, T, num_units/2].
? ? '''
? ? with tf.variable_scope(scope, reuse=reuse):
? ? ? ? outputs = tf.layers.dense(inputs, units=hp.embed_size, activation=tf.nn.relu, name="dense1")
? ? ? ? outputs = tf.nn.dropout(outputs, keep_prob=.5 if is_training==True else 1., name="dropout1")
? ? ? ? outputs = tf.layers.dense(outputs, units=hp.embed_size//2, activation=tf.nn.relu, name="dense2")
? ? ? ? outputs = tf.nn.dropout(outputs, keep_prob=.5 if is_training==True else 1., name="dropout2")
? ? return outputs # (N, T, num_units/2)
CBHG Module
CBHG模塊由1-D convolution bank 蹦疑,highway network 西雀,bidirectional GRU 組成。它的功能是從輸入中提取有價值的特征歉摧,有利于提高模型的泛化能力艇肴。
1-D convolution bank:
輸入序列首先會經過一個卷積層,注意這個卷積層叁温,它有K個大小不同的1維的filter再悼,其中filter的大小為1,2,3…K。這些大小不同的卷積核提取了長度不同的上下文信息膝但。其實就是n-gram語言模型的思想冲九,K的不同對應了不同的gram, 例如unigrams, bigrams, up to K-grams,然后锰镀,將經過不同大小的k個卷積核的輸出堆積在一起(注意:在做卷積時娘侍,運用了padding咖刃,因此這k個卷積核輸出的大小均是相同的),也就是把不同的gram提取到的上下文信息組合在一起憾筏,下一層為最大池化層嚎杨,stride為1,width為2氧腰。代碼如下:
def conv1d_banks(inputs, K=16, is_training=True, scope="conv1d_banks", reuse=None):
? ? '''Applies a series of conv1d separately.
? ? Args:
? ? ? inputs: A 3d tensor with shape of [N, T, C]
? ? ? K: An int. The size of conv1d banks. That is,
? ? ? ? The `inputs` are convolved with K filters: 1, 2, ..., K.
? ? ? is_training: A boolean. This is passed to an argument of `batch_normalize`.
? ? Returns:
? ? ? A 3d tensor with shape of [N, T, K*Hp.embed_size//2].
? ? '''
? ? with tf.variable_scope(scope, reuse=reuse):
? ? ? ? outputs = conv1d(inputs, hp.embed_size//2, 1) # k=1
? ? ? ? for k in range(2, K+1): # k = 2...K
? ? ? ? ? ? with tf.variable_scope("num_{}".format(k)):
? ? ? ? ? ? ? ? output = conv1d(inputs, hp.embed_size // 2, k)
? ? ? ? ? ? ? ? outputs = tf.concat((outputs, output), -1)
? ? ? ? outputs = normalize(outputs, type=hp.norm_type, is_training=is_training,
? ? ? ? ? ? ? ? ? ? ? ? ? ? activation_fn=tf.nn.relu)
? ? return outputs # (N, T, Hp.embed_size//2*K)
注意ouputs = normalize(outputs, type=hp.norm_type, is_training=is_training, activation_fn=tf.nn.relu)這行代碼枫浙,這里對output做了batch normalization處理(每個mini_batch中),至于BN的作用古拴,網(wǎng)上有很多資料箩帚,我這里就簡單說下,我們知道不加BN的神經網(wǎng)絡黄痪,每層都會有一個非線性話操作紧帕,如sigmoid或者RELU,這樣一方面每層的輸入分布和上一層都不相同桅打,這樣會使得模型的收斂和預測能力下降是嗜。其次,對于深層網(wǎng)絡而言挺尾,這樣會帶來梯度彌散和梯度爆炸的問題鹅搪,因為模型在back propogation的時候是依據(jù)鏈式法則,深度越深遭铺,問題越嚴重丽柿,BN引入其他參數(shù)抹去了w的scale的影響。公式網(wǎng)上都有魂挂,這里就不在搬了甫题。
經過池化之后,會再經過兩層一維的卷積層涂召。第一個卷積層的filter大小為3幔睬,stride為1,采用的激活函數(shù)為ReLu芹扭;第二個卷積層的filter大小為3,stride為1赦抖,沒有采用激活函數(shù)(在這兩個一維的卷積層之間都會進行batch normalization)舱卡。
residual connection:
經過卷積層之后,會進行一個residual connection队萤。也就是把卷積層輸出的和embeding之后的序列相加起來轮锥。使用residual connection也是一個緩解神經網(wǎng)絡太深帶來的梯度彌散問題的方法。我們知道要尔,在訓練神經網(wǎng)絡的時候舍杜,一個合適的layer size是很重要的新娜,網(wǎng)絡太深,會帶來梯度彌散的問題既绩,太淺又不能很好的學到特征概龄,residual connection可以緩解網(wǎng)絡太深帶來的問題,就是把輸入和經過卷積后結果相加饲握,這樣可以確保經過多層卷積后私杜,沒有丟失太多之前輸入的信息。代碼如下:
enc += prenet_out # (N, T, E/2) # residual connections
這里就是把前面prenet層后經過多層卷積后的結果和prenet的輸出相加救欧。
highway network:
下一層輸入到highway layers衰粹,highway nets的每一層結構為:把輸入同時放入到兩個一層的全連接網(wǎng)絡中,這兩個網(wǎng)絡的激活函數(shù)分別采用了ReLu和sigmoid函數(shù)笆怠,假定輸入為input铝耻,ReLu的輸出為output1,sigmoid的輸出為output2蹬刷,那么highway layer的輸出為output=output1?output2+input?(1?output2)瓢捉。論文中使用了4層highway layer。
代碼如下:
def highwaynet(inputs, num_units=None, scope="highwaynet", reuse=None):
? ? '''Highway networks, see https://arxiv.org/abs/1505.00387
? ? Args:
? ? ? inputs: A 3D tensor of shape [N, T, W].
? ? ? num_units: An int or `None`. Specifies the number of units in the highway layer
? ? ? ? ? ? or uses the input size if `None`.
? ? ? scope: Optional scope for `variable_scope`.?
? ? ? reuse: Boolean, whether to reuse the weights of a previous layer
? ? ? ? by the same name.
? ? Returns:
? ? ? A 3D tensor of shape [N, T, W].
? ? '''
? ? if not num_units:
? ? ? ? num_units = inputs.get_shape()[-1]
? ? with tf.variable_scope(scope, reuse=reuse):
? ? ? ? H = tf.layers.dense(inputs, units=num_units, activation=tf.nn.relu, name="dense1")
? ? ? ? T = tf.layers.dense(inputs, units=num_units, activation=tf.nn.sigmoid, name="dense2")
? ? ? ? C = 1. - T
? ? ? ? outputs = H * T + inputs * C
? ? return outputs
從代碼中我們也能看出highway network公式為:
其中C等于1-T箍铭,x為輸入泊柬, y 為對應的輸出,T為transfer gate诈火,C為carry gate兽赁,其實就是讓網(wǎng)絡的輸出由兩部分組成,分別是網(wǎng)絡的直接輸入以及輸入變形后的部分冷守。為什么要使用highway network的節(jié)奏呢刀崖,其實說白了也是一種減少緩解網(wǎng)絡加深帶來過擬合問題,以及減少較深網(wǎng)絡的訓練難度的一個trick拍摇。它主要受到LSTM門限機制的啟發(fā)亮钦。
GRU:
然后將輸出輸入到雙向的GRU中,從GRU中輸出的結果就是encoder的輸出充活。GRU是RNN的一個變體蜂莉,它和LSTM一樣都使用了門限機制,不同的是它只有更新門和重置門混卵。公式如下:
Reset gate
r(t) 負責決定h(t?1) 對new memory h^(t) 的重要性有多大映穗, 如果r(t)約等于0 的話,h(t?1) 就不會傳遞給new memory h^(t)
new memory
h^(t) 是對新的輸入x(t) 和上一時刻的hidden state h(t?1) 的總結幕随。計算總結出的新的向量h^(t) 包含上文信息和新的輸入x(t).
Update gate
z(t) 負責決定傳遞多少ht?1給ht 蚁滋。 如果z(t) 約等于1的話,ht?1 幾乎會直接復制給ht ,相反辕录,如果z(t) 約等于0睦霎, new memory h^(t) 直接傳遞給ht.
Hidden state:
h(t) 由 h(t?1) 和)h^(t) 相加得到,兩者的權重由update gate z(t) 控制走诞。
def gru(inputs, num_units=None, bidirection=False, scope="gru", reuse=None):
? ? '''Applies a GRU.
? ? Args:
? ? ? inputs: A 3d tensor with shape of [N, T, C].
? ? ? num_units: An int. The number of hidden units.
? ? ? bidirection: A boolean. If True, bidirectional results
? ? ? ? are concatenated.
? ? ? scope: Optional scope for `variable_scope`.?
? ? ? reuse: Boolean, whether to reuse the weights of a previous layer
? ? ? ? by the same name.
? ? Returns:
? ? ? If bidirection is True, a 3d tensor with shape of [N, T, 2*num_units],
? ? ? ? otherwise [N, T, num_units].
? ? '''
? ? with tf.variable_scope(scope, reuse=reuse):
? ? ? ? if num_units is None:
? ? ? ? ? ? num_units = inputs.get_shape().as_list[-1]
? ? ? ? cell = tf.contrib.rnn.GRUCell(num_units)?
? ? ? ? if bidirection:
? ? ? ? ? ? cell_bw = tf.contrib.rnn.GRUCell(num_units)
? ? ? ? ? ? outputs, _ = tf.nn.bidirectional_dynamic_rnn(cell, cell_bw, inputs,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? dtype=tf.float32)
? ? ? ? ? ? return tf.concat(outputs, 2)?
? ? ? ? else:
? ? ? ? ? ? outputs, _ = tf.nn.dynamic_rnn(cell, inputs,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? dtype=tf.float32)
? ? ? ? ? ? return outputs
到這里副女,CBHG的部分(encoder)就總結完畢了,CBHG的代碼:
def encode(inputs, is_training=True, scope="encoder", reuse=None):
? ? '''
? ? Args:
? ? ? inputs: A 2d tensor with shape of [N, T], dtype of int32.
? ? ? seqlens: A 1d tensor with shape of [N,], dtype of int32.
? ? ? masks: A 3d tensor with shape of [N, T, 1], dtype of float32.
? ? ? is_training: Whether or not the layer is in training mode.
? ? ? scope: Optional scope for `variable_scope`
? ? ? reuse: Boolean, whether to reuse the weights of a previous layer
? ? ? ? by the same name.
? ? Returns:
? ? ? A collection of Hidden vectors, whose shape is (N, T, E).
? ? '''
? ? with tf.variable_scope(scope, reuse=reuse):
? ? ? ? # Load vocabulary
#? ? ? ? char2idx, idx2char = load_vocab()
? ? ? ? vocab, revocab = load_vocab()
? ? ? ? # Character Embedding
? ? ? ? inputs = embed(inputs, len(vocab), hp.embed_size) # (N, T, E)?
? ? ? ? # Encoder pre-net
? ? ? ? prenet_out = prenet(inputs, is_training=is_training) # (N, T, E/2)
? ? ? ? # Encoder CBHG
? ? ? ? ## Conv1D bank
? ? ? ? enc = conv1d_banks(prenet_out, K=hp.encoder_num_banks, is_training=is_training) # (N, T, K * E / 2)
? ? ? ? ### Max pooling
? ? ? ? enc = tf.layers.max_pooling1d(enc, 2, 1, padding="same")? # (N, T, K * E / 2)
? ? ? ? ### Conv1D projections
? ? ? ? enc = conv1d(enc, hp.embed_size//2, 3, scope="conv1d_1") # (N, T, E/2)
? ? ? ? enc = normalize(enc, type=hp.norm_type, is_training=is_training,
? ? ? ? ? ? ? ? ? ? ? ? ? ? activation_fn=tf.nn.relu, scope="norm1")
? ? ? ? enc = conv1d(enc, hp.embed_size//2, 3, scope="conv1d_2") # (N, T, E/2)
? ? ? ? enc = normalize(enc, type=hp.norm_type, is_training=is_training,
? ? ? ? ? ? ? ? ? ? ? ? ? ? activation_fn=None, scope="norm2")
? ? ? ? enc += prenet_out # (N, T, E/2) # residual connections
? ? ? ? ### Highway Nets
? ? ? ? for i in range(hp.num_highwaynet_blocks):
? ? ? ? ? ? enc = highwaynet(enc, num_units=hp.embed_size//2,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? scope='highwaynet_{}'.format(i)) # (N, T, E/2)
? ? ? ? ### Bidirectional GRU
? ? ? ? memory = gru(enc, hp.embed_size//2, True) # (N, T, E)
? ? return memory
decoder模塊
decoder模塊主要分為三部分:
- pre-net
- Attention-RNN
- Decoder-RNN
Pre-net的結構與encoder中的pre-net相同速梗,主要是對輸入做一些非線性變換肮塞。
Attention-RNN的結構為一層包含256個GRU的RNN,它將pre-net的輸出和attention的輸出作為輸入姻锁,經過GRU單元后輸出到decoder-RNN中枕赵。
Decode-RNN為兩層residual GRU,它的輸出為輸入與經過GRU單元輸出之和位隶。每層同樣包含了256個GRU單元拷窜。第一步decoder的輸入為0矩陣,之后都會把第t步的輸出作為第t+1步的輸入涧黄。(這里paper中使用了一個trick篮昧,就是每次decoder的時候,不僅僅預測1幀的數(shù)據(jù)笋妥,而是預測多個非重疊的幀懊昨。因為就像我們前面說到的提取音頻特征的時候,我們會先分幀春宣,相鄰的幀其實是有一定的關聯(lián)性的酵颁,所以每個字符在發(fā)音的時候,可能對應了多個幀月帝,因此每個GRU單元輸出為多個幀的音頻文件躏惋。)
為什么這樣做呢:
- 減小了model size,比如當前需要預測n個幀嚷辅,如果每次一個幀的話簿姨,就對應了n個GRU,而如果每次預測r個幀的話簸搞,只需要n/r個GRU扁位,從而減小了模型的復雜度
- 減少了模型訓練和預測的時間
- 提高收斂的速度
其實下兩點好處都是第一點好處帶來的。代碼如下:
def decode1(decoder_inputs, memory, is_training=True, scope="decoder1", reuse=None):
? ? '''
? ? Args:
? ? ? decoder_inputs: A 3d tensor with shape of [N, T', C'], where C'=hp.n_mels*hp.r,
? ? ? ? dtype of float32. Shifted melspectrogram of sound files.
? ? ? memory: A 3d tensor with shape of [N, T, C], where C=hp.embed_size.
? ? ? is_training: Whether or not the layer is in training mode.
? ? ? scope: Optional scope for `variable_scope`
? ? ? reuse: Boolean, whether to reuse the weights of a previous layer
? ? ? ? by the same name.
? ? Returns
? ? ? Predicted melspectrogram tensor with shape of [N, T', C'].
? ? '''
? ? with tf.variable_scope(scope, reuse=reuse):
? ? ? ? # Decoder pre-net
? ? ? ? dec = prenet(decoder_inputs, is_training=is_training) # (N, T', E/2)
? ? ? ? # Attention RNN
? ? ? ? dec = attention_decoder(dec, memory, num_units=hp.embed_size) # (N, T', E)
? ? ? ? # Decoder RNNs
? ? ? ? dec += gru(dec, hp.embed_size, False, scope="decoder_gru1") # (N, T', E)
? ? ? ? dec += gru(dec, hp.embed_size, False, scope="decoder_gru2") # (N, T', E)
? ? ? ? # Outputs => (N, T', hp.n_mels*hp.r)
? ? ? ? out_dim = decoder_inputs.get_shape().as_list()[-1]
? ? ? ? outputs = tf.layers.dense(dec, out_dim)
? ? return outputs
post-processing
和seq2seq網(wǎng)絡不通的是趁俊,tacotron在decoder-RNN輸出之后并沒有直將其作為輸出通過Griffin-Lim算法合成音頻贤牛,而是添加了一層post-processing模塊。
為什么要添加這一層呢则酝?
首先是因為我們使用了Griffin-Lim重建算法,根據(jù)頻譜生成音頻,Griffin-Lim原理是:我們知道相位是描述波形變化的沽讹,我們從頻譜生成音頻的時候般卑,需要考慮連續(xù)幀之間相位變化的規(guī)律,如果找不到這個規(guī)律爽雄,生成的信號和原來的信號肯定是不一樣的蝠检,Griffin Lim算法解決的就是如何不弄壞左右相鄰的幅度譜和自身幅度譜的情況下,求一個近似的相位挚瘟,因為相位最差和最好情況下天壤之別叹谁,所有應該會有一個相位變化的迭代方案會比上一次更好一點,而Griffin Lim算法找到了這個方案乘盖。這里說了這么多焰檩,其實就是Griffin-Lim算法需要看到所有的幀。post-processing可以在一個線性頻率范圍內預測幅度譜(spectral magnitude)订框。
其次析苫,post-processing能看到整個解碼的序列,而不像seq2seq那樣穿扳,只能從左至右的運行衩侥。它能夠通過正向傳播和反向傳播的結果來修正每一幀的預測錯誤。
論文中使用了CBHG的結構來作為post-processing net矛物,前面已經詳細介紹過茫死。代碼如下:
def decode2(inputs, is_training=True, scope="decoder2", reuse=None):
? ? '''
? ? Args:
? ? ? inputs: A 3d tensor with shape of [N, T', C'], where C'=hp.n_mels*hp.r,
? ? ? ? dtype of float32. Log magnitude spectrogram of sound files.
? ? ? is_training: Whether or not the layer is in training mode.?
? ? ? scope: Optional scope for `variable_scope`
? ? ? reuse: Boolean, whether to reuse the weights of a previous layer
? ? ? ? by the same name.
? ? Returns
? ? ? Predicted magnitude spectrogram tensor with shape of [N, T', C''],
? ? ? ? where C'' = (1+hp.n_fft//2)*hp.r.
? ? '''
? ? with tf.variable_scope(scope, reuse=reuse):
? ? ? ? # Decoder pre-net
? ? ? ? prenet_out = prenet(inputs, is_training=is_training) # (N, T'', E/2)
? ? ? ? # Decoder Post-processing net = CBHG
? ? ? ? ## Conv1D bank
? ? ? ? dec = conv1d_banks(prenet_out, K=hp.decoder_num_banks, is_training=is_training) # (N, T', E*K/2)
? ? ? ? ## Max pooling
? ? ? ? dec = tf.layers.max_pooling1d(dec, 2, 1, padding="same") # (N, T', E*K/2)
? ? ? ? ## Conv1D projections
? ? ? ? dec = conv1d(dec, hp.embed_size, 3, scope="conv1d_1") # (N, T', E)
? ? ? ? dec = normalize(dec, type=hp.norm_type, is_training=is_training,
? ? ? ? ? ? ? ? ? ? ? ? ? ? activation_fn=tf.nn.relu, scope="norm1")
? ? ? ? dec = conv1d(dec, hp.embed_size//2, 3, scope="conv1d_2") # (N, T', E/2)
? ? ? ? dec = normalize(dec, type=hp.norm_type, is_training=is_training,
? ? ? ? ? ? ? ? ? ? ? ? ? ? activation_fn=None, scope="norm2")
? ? ? ? dec += prenet_out
? ? ? ? ## Highway Nets
? ? ? ? for i in range(4):
? ? ? ? ? ? dec = highwaynet(dec, num_units=hp.embed_size//2,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? scope='highwaynet_{}'.format(i)) # (N, T, E/2)
? ? ? ? ## Bidirectional GRU? ?
? ? ? ? dec = gru(dec, hp.embed_size//2, True) # (N, T', E)?
? ? ? ? # Outputs => (N, T', (1+hp.n_fft//2)*hp.r)
? ? ? ? out_dim = (1+hp.n_fft//2)*hp.r
? ? ? ? outputs = tf.layers.dense(dec, out_dim)
? ? return outputs
最后使用Griffin-Lim算法來將post-processing net的輸出合成為語音。到這里就基本結束了履羞,感興趣的話可以看下論文加深理解:?https://arxiv.org/pdf/1703.10135.pdf
原文參考:https://blog.csdn.net/yunnangf/article/details/79585089