今回はTransformerを改造して、文章から音声を生成してみた。
俗に言う、エンコーダ・デコーダ形式のモデル。
プロセスが長くなりそうなので、要点だけ備忘録もかねてまとめようと思う。
目次
1.Transformer-TextToSpeechとは?
2.テキスト前処理
3.TransformerとPostNetの学習
4.テキストから音声を作成してみる
1.transformer-TextToSpeechとは?
今回作ったtransformerでの音声生成は、Google が発表したTactron2を改造した。
tactron2のEncoderとDecoderをTransformerに置き換えて、waveGlowをpostnetに置き換えたモデル。
Tactron2はそもそもGoogleのこれまでの音声生成プロジェクトで作られた、WaveNetと初代Tacotronのネットワークを組み合わせたもので、詳しくはサイトを見て欲しい。
tacotron2での音声生成処理の流れ
今回作ったTransformerの音声生成処理の流れ
つまり、
・waveGlow => PostNet
単純なAttentionモデルのseq2seqを使った場合より、4倍くらい速く、精度もbetter。
特にtransformerのattentionの部分が正確な音声の作成にかなり有効っぽい。
2.テキスト前処理
今回データセットは「The LJ Speech Dataset」を使った。
英語での音声を想定したデータセット。
前処理では、日本語でもローマ字(英語のスペル)に変換する。
今回は英語対応なので、すべての文字を英語のスペルに変換。
試しにデータセットの一部分の、LJ050-0278のデータをのぞいてみる。
textデータ(csv)
LJ050-0278|the recommendations we have here suggested would greatly advance the security of the office without any impairment of our fundamental liberties.|the recommendations we have here suggested would greatly advance the security of the office without any impairment of our fundamental liberties.
音声データ
LJ050-0278.mag.npy LJ050-0278.pt.npy LJ050-0278.wav
まず訓練前にこのテキストデータを前処理した。
3.TransformerとPostNetの学習
まず、Transformerを学習させてから、最後にPostNetを学習して、音声合成する。
Transformerはスペクトログラム(人間の音高知覚に調整した特徴量:メル周波数)を出力。
PostNetは音声の波形データを出力する。
Pytorchなのでmodelをprint()してみた
Transformer
model = Transformer_model() print(model) >>>>> Model( (encoder): Encoder( (pos_emb): Embedding(1024, 256) (pos_dropout): Dropout(p=0.1, inplace=False) (encoder_prenet): EncoderPrenet( (embed): Embedding(149, 512, padding_idx=0) (conv1): Conv( (conv): Conv1d(512, 256, kernel_size=(5,), stride=(1,), padding=(2,)) ~略~ (1): Attention( (key): Linear( (linear_layer): Linear(in_features=256, out_features=256, bias=False) ) (value): Linear( (linear_layer): Linear(in_features=256, out_features=256, bias=False) ) (query): Linear( (linear_layer): Linear(in_features=256, out_features=256, bias=False) ~略~ (decoder): MelDecoder( (pos_emb): Embedding(1024, 256) (pos_dropout): Dropout(p=0.1, inplace=False) (decoder_prenet): Prenet( (layer): Sequential( (fc1): Linear( (linear_layer): Linear(in_features=80, out_features=512, bias=True) ) ~略~ (pre_batchnorm): BatchNorm1d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (dropout1): Dropout(p=0.1, inplace=False) (dropout_list): ModuleList( (0): Dropout(p=0.1, inplace=False) (1): Dropout(p=0.1, inplace=False) (2): Dropout(p=0.1, inplace=False) ) ) ) )
PostNet
model = PostNet_model() print(model) >>>>>> ModelPostNet( (pre_projection): Conv( (conv): Conv1d(80, 256, kernel_size=(1,), stride=(1,)) ) (cbhg): CBHG( (convbank_list): ModuleList( (0): Conv1d(256, 256, kernel_size=(1,), stride=(1,)) (1): Conv1d(256, 256, kernel_size=(2,), stride=(1,), padding=(1,)) (2): Conv1d(256, 256, kernel_size=(3,), stride=(1,), padding=(1,)) (3): Conv1d(256, 256, kernel_size=(4,), stride=(1,), padding=(2,)) (4): Conv1d(256, 256, kernel_size=(5,), stride=(1,), padding=(2,)) (5): Conv1d(256, 256, kernel_size=(6,), stride=(1,), padding=(3,)) (6): Conv1d(256, 256, kernel_size=(7,), stride=(1,), padding=(3,)) (7): Conv1d(256, 256, kernel_size=(8,), stride=(1,), padding=(4,)) ~略~ (linears): ModuleList( (0): Linear( (linear_layer): Linear(in_features=256, out_features=256, bias=True) ) (1): Linear( (linear_layer): Linear(in_features=256, out_features=256, bias=True) ) (2): Linear( (linear_layer): Linear(in_features=256, out_features=256, bias=True) ) (3): Linear( (linear_layer): Linear(in_features=256, out_features=256, bias=True) ) ) ) (gru): GRU(256, 128, num_layers=2, batch_first=True, bidirectional=True) ) (post_projection): Conv( (conv): Conv1d(256, 1025, kernel_size=(1,), stride=(1,)) ) )
コードだと分かりにくいのでモデルの全体像。
学習はfine-tuneで行った。
学習率のlearning rateは0.001がベストプラクティス。
あと学習が進むにつれてlearning rateも下がる調整もかなり重要っぽい
def adjust_learning_rate(optimizer, step_num, warmup_step=4000): lr = hp.lr * warmup_step**0.5 * min(step_num * warmup_step**-1.5, step_num**-0.5) for param_group in optimizer.param_groups: param_group['lr'] = lr
loss曲線はtctron2からの引用だけど、同等もさくはそれ以上に、かなりいい具合に減少。
でかいデータセットでやるとメモリエラーも起こるし、時間もとんでもなくかかるからfine-tuneがおすすめ。
最近はもうfine-tuneの方がかなり効率いいので、end-to-endの学習は余程のことがないとしないんじゃないかな?
4.テキストから音声を作成してみる
映画ホームアローンのハリーのセリフを作成してみる。
text1 = "I never made it to sixth grade"
text2 = "it dose not look like you are gonna"
周波数(fs)は低くするとドスのきいた声になり、高いと早口言葉みたいになる。
max_lenはtextの長さに比例するのでそれぞれちょうどいい具合に調整した。
def calculate_melsp(x, n_fft=1024, hop_length=128, n_mels=128): stft = np.abs(librosa.stft(x, n_fft=n_fft, hop_length=hop_length))**2 log_stft = librosa.power_to_db(stft) melsp = librosa.feature.melspectrogram(S=log_stft, n_mels=n_mels) return melsp # display wave in plots def show_wave(x): plt.plot(x) plt.show() # display wave in heatmap def show_melsp(melsp, fs): librosa.display.specshow(melsp, sr=fs) plt.colorbar() plt.show()
text1 = "I never made it to sixth grade, kid."
max_len = 500 fs = 25000 text1 = "I never made it to sixth grade, kid." wav = create_audio_wave(text1, max_len) print(wav.shape) # (137225,) show_wave(wav) melsp = calculate_melsp(wav, n_fft=fs, hop_length=max_len, n_mels=max_len) print(melsp.shape) # (500, 275) show_melsp(melsp, fs) # 実際にjupyter上で音声が聞ける ipd.Audio(wav, rate=fs)
text2 = "it dose not look like you are gonna"
text2 = "it dose not look like you are gonna"
wav = create_audio_wave(text2, max_len)
ipd.Audio(wav, rate=fs)
AttentionはNLPだけじゃなく、いろんな精度向上に役立つっぽい。
ガチの音声生成をしたのははじめてだった。分類系より、生成系は面白い。
参考サイト
・GitHub - soobinseo/Transformer-TTS: A Pytorch Implementation of "Neural Speech Synthesis with Transformer Network"