ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Tacotron 무지성 구현 - 4/N
    Tacotron 1 2021. 7. 29. 01:03

    이번 포스팅에서는 Encoder를 구현해보려고 합니다.

     

    Encoder의 Embedding, PreNet, CBHG 각 Layer들의 차원을 맞추는 과정에서

    조금 어려운 부분이 있었습니다. 

     

    제가 헤맸던 부분은,

    Embeding Layer의 인자 중 하나인 embedding_dim을

    고정된 값으로 생각해서 구현하다가

    잘못된 것을 깨닫고 처음부터 다시 코드를 작성했습니다 :(

     

    이 부분은 Decoder와 연관이 있을 것이므로 잘 생각해보고

    구현해야 할 듯합니다.

     

    전처리 부분은 그럭저럭 말로 설명이 잘 됐는데,

    Encoder를 포함한 앞으로 구현할 Decoder, Post CBHG 포스팅에서는 

    조금 난해한 설명이 있을 수도 있으니 양해 부탁드립니다.

     

    작성되고 나서도 설명 부분은 제가 수정할 수 있으니 양해 바랍니다.

     

     

     


    Table & Encoder Structure

     

    아래의 이미지는 논문 5페이지 상단 부분에 있는 Table 1입니다.

    앞으로 구현할 때 많이 참고할 것 같습니다.

    Encoder, Decoder를 구현할 때 Hyper Parameters 인자들은 모두 Tacotron 논문에 기재된 내용을 바탕으로 작성할 예정입니다.
    Encoder 구조입니다.

     

     

     


    Hyper Parameters

     

    import os
    
    class Hparams():
        # speaker name
        speaker = 'KSS'
        
        # Audio Pre-processing
        origin_sample_rate = 44100
        sample_rate = 22050
        n_fft = 1024
        hop_length = 256
        win_length = 1024
        n_mels = 80
        reduction = 5
        n_specs = n_fft // 2 + 1
        fmin = 0
        fmax = sample_rate // 2
        min_level_db = -80
        ref_level_db = 0
        
        # Text Pre-processing
        PAD = '_'
        EOS = '~'
        SPACE = ' '
        SPECIAL = '.,!?'
        JAMO_LEADS = "".join([chr(_) for _ in range(0x1100, 0x1113)])
        JAMO_VOWELS = "".join([chr(_) for _ in range(0x1161, 0x1176)])
        JAMO_TAILS = "".join([chr(_) for _ in range(0x11A8, 0x11C3)])
        symbols = PAD + EOS + JAMO_LEADS + JAMO_VOWELS + JAMO_TAILS + SPACE + SPECIAL
    
        _symbol_to_id = {s: i for i, s in enumerate(symbols)}
        _id_to_symbol = {i: s for i, s in enumerate(symbols)}
        
        # Pre-processing paths (text, mel, spec)
        data_dir = os.path.join('data')
        out_texts_dir = os.path.join(data_dir, 'texts')
        out_mels_dir = os.path.join(data_dir, 'mels')
        out_specs_dir = os.path.join(data_dir, 'specs')
        
        # Embedding Layer
        in_dim = 256
        
        # Encoder Pre-net Layer
        prenet_dropout_ratio = 0.5
        prenet_linear_size = 256
    
        # CBHG
        cbhg_K = 16
        cbhg_mp_k = 2
        cbhg_mp_s = 1
        cbhg_mp_p = 1
        cbhg_mp_d = 2
        cbhg_conv_proj_size = 128
        cbhg_conv_proj_k = 3
        cbhg_conv_proj_p = 1    
        cbhg_gru_hidden_dim = 128

    Hparams 클래스에 #Embedding Layer 주석 부분부터 새롭게 추가된 인자들입니다.

     

    in_dim 인자는 Embedding Layer에서

    embedding_dim 인자 값으로 입력될 값입니다.

     

    prenet_dropout_ratio와 prenet_linear_size 인자는 PreNet에 들어갈 인자들이며,

    Table 1을 참고했습니다.

     

    아래의 이미지는 논문 3페이지 내용 중 CBHG MODULE 부분입니다.

    cbhg_K 인자는 자연어처리에서 N-gram의 효과를 보기 위해서 Conv1d Layer에 

    적용했다고 합니다.

    논문 Table 1에서는 Encoder CBHG K 값을 16으로 기재했으므로,

    저는 cbhg_K 라는 변수에 16으로 지정했습니다.

    N-gram과 관련된 내용은 아래의 링크를 참고해주세요.

    https://wikidocs.net/21692

     

    위키독스

    온라인 책을 제작 공유하는 플랫폼 서비스

    wikidocs.net

    인자 이름 끝 자리에 알파벳 하나처럼 이니셜만 있는 인자들은

    MaxPool1d, Conv1d Layer에서

    kernel_size, stride, padding, dilation의 앞 단어를 따온 인자들입니다.

    cbhg_conv_proj_k 값을 제외하고는 전부 차원을 맞추기 위해

    제가 별도로 추가한 인자들입니다.

     

    cbhg_conv_proj_size와 cbhg_gru_hidden_dim 인자 또한 Table 1에 기재된

    값을 기준으로 Hparams에 추가했습니다.

     

     

     


    CustomLinear & BatchNormConv1d Layer

     

    이미지에서 드래그된 부분을 보면, 

    "Batch normalization is used for all convolusional layers."

    라고 적혀있습니다.

    이 말은 CBHG에 들어가는 모든 Conv1d Layer는

    BatchNorm1d Layer와 붙어 다닌다는 뜻입니다.

     

    또한, FC라고 적혀있는 부분은 Fully Connected라는 말일 텐데,

    아마 pytorch에서는 torch.nn.Linear을 사용하는 것과 같은 의미일 것입니다.

    그리고 FC와 ReLU, Dropout이 자주 붙어있는 것을 볼 수 있습니다.

     

    저는 언급한 두 부분의 Layer들을 각각 결합하여

    한 번에 사용할 수 있게끔 만들도록 하겠습니다.

    class CustomLinear(torch.nn.Module):
        def __init__(self, in_features, out_features, 
                            bias=True, device=None, dtype=None, 
                            activation=None, dropout=None):
            super(CustomLinear, self).__init__()
            self.in_features = in_features
            self.out_features = out_features
            self.bias = bias
            self.device = device
            self.dtype = dtype
            self.dropout = dropout
            self.activation = activation
            self.linear = torch.nn.Linear(in_features=self.in_features, 
                                          out_features=self.out_features,
                                          bias=self.bias,
                                          device=self.device,
                                          dtype=self.dtype)
            
            # activation functions
            if self.activation != None:
                if self.activation == 'relu':
                    self.activation = torch.nn.ReLU()
                elif self.activation == 'leakyrelu':
                    self.activation = torch.nn.LeakyReLU()
                elif self.activation == 'sigmoid':
                    self.activation = torch.nn.Sigmoid()
                elif self.activation == 'tanh':
                    self.activation = torch.nn.Tanh()
                else:
                    self.activation = torch.nn.ReLU()
            else:
                self.activation = None
                
                
            # dropout function
            if self.dropout != None:
                self.dropout = torch.nn.Dropout(self.dropout)
            else:
                self.dropout = None
            
            
        def forward(self, x):
            x = self.linear(x)
            
            if self.activation != None:
                x = self.activation(x)
                
            if self.dropout != None:
                x = self.dropout(x)
                
            return x
    linear = CustomLinear(10, 10, 1, activation='relu', dropout=0.5)
    
    print(linear)
    
    
    ####
    CustomLinear(
      (linear): Linear(in_features=10, out_features=10, bias=True)
      (activation): ReLU()
      (dropout): Dropout(p=0.5, inplace=False)
    )

     

     

    class BatchNormConv1D(torch.nn.Module):
        def __init__(self, in_channels, out_channels, kernel_size, 
                            stride=1, padding=0, dilation=1, groups=1, eps=1e-05, momentum=0.1,
                            affine=True, track_running_stats=True, 
                            bias=True, padding_mode='zeros', device=None, dtype=None,
                            activation=None):
            super(BatchNormConv1D, self).__init__()
            self.in_channels = in_channels
            self.out_channels = out_channels
            self.kernel_size = kernel_size
            self.stride = stride
            self.padding = padding
            self.dilation = dilation
            self.groups = groups
            self.eps = eps
            self.momentum = momentum
            self.affine = affine
            self.track_running_stats = track_running_stats
            self.bias = bias
            self.padding_mode = padding_mode
            self.device = device
            self.dtype = dtype
            self.activation = activation
            
            self.conv1d = torch.nn.Conv1d(in_channels=self.in_channels, 
                                          out_channels=self.out_channels, 
                                          kernel_size=self.kernel_size, 
                                          stride=self.stride, 
                                          padding=self.padding, 
                                          dilation=self.dilation, 
                                          groups=self.groups, 
                                          bias=self.bias, 
                                          padding_mode=self.padding_mode, 
                                          device=self.device, 
                                          dtype=self.dtype)
            
            self.batchnorm1d = torch.nn.BatchNorm1d(num_features=self.out_channels,
                                                    eps=self.eps,
                                                    momentum=self.momentum,
                                                    affine=self.affine,
                                                    track_running_stats=self.track_running_stats,
                                                    device=self.device,
                                                    dtype=self.dtype)
    
            # activation functions
            if self.activation != None:
                if self.activation == 'relu':
                    self.activation = torch.nn.ReLU()
                elif self.activation == 'leakyrelu':
                    self.activation = torch.nn.LeakyReLU()
                elif self.activation == 'sigmoid':
                    self.activation = torch.nn.Sigmoid()
                elif self.activation == 'tanh':
                    self.activation = torch.nn.Tanh()
                else:
                    self.activation = torch.nn.ReLU()
            else:
                self.activation = None
                
            
        def forward(self, x):
            x = self.conv1d(x)
            x = self.batchnorm1d(x)
            
            if self.activation != None:
                x = self.activation(x)
                
            return x
    bnc1d = BatchNormConv1D(10, 10, 1, activation='relu')
    
    print(bnc1d)
    
    
    ####
    BatchNormConv1D(
      (conv1d): Conv1d(10, 10, kernel_size=(1,), stride=(1,))
      (batchnorm1d): BatchNorm1d(10, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (activation): ReLU()
    )

    조금 많이 복잡해 보이지만,

    사실 pytorch tutorial에서 기본적인 인자들만 모두 가져와서

    그대로 적어놓은 것뿐입니다.

    사용 방법은 각 레이어를 정의해놓은 코드블럭 밑에

    따로 옮겨두었으니 참고하면 됩니다.

     

     

     


    Embedding Layer

     

    사실 Embedding Layer는 별도의 클래스로 만들 필요도 없고,

    Encoder에 포함할 필요도 없긴 합니다.

    그래도 저는 깔끔한 것을 좋아하기 때문에 따로 만들도록 하겠습니다.

    class Embedding(torch.nn.Module):
        def __init__(self, Hparams):
            super(Embedding, self).__init__()
            self.Hparams = Hparams
            layers = [torch.nn.Embedding(num_embeddings=len(self.Hparams.symbols), 
                                         embedding_dim=self.Hparams.in_dim)]
    
            self.embedding_layer = torch.nn.Sequential(*layers)
    
        def forward(self, x):
            return self.embedding_layer(x)

    Encoder Layer를 통과한 Text의 결과값은 아래와 같습니다.

    Hparams에서 in_dim=512를 지정한 것과 같이

    가장 마지막 shape가 in_dim과 동일한 것을 볼 수 있습니다.

     

    아래의 이미지에서 볼 수 있듯이

    논문에서는 in_dim(embedding_dim) 값을 256으로 지정했습니다.

    저는 처음에 언급했던 문제점을 해결하다 보니

    in_dim(embedding_dim)을 512로 두고 진행했습니다.

    다음 포스팅에서 학습을 진행할 때는 256으로 맞춰놓고 진행할 예정입니다.

     

    그리고 지금은 학습 중이 아니라 실험 중이므로 batch_size를 4로 지정했습니다.

    dataset = KSSDataset(hparams)
    
    dataloader = torch.utils.data.DataLoader(dataset, 
                                             batch_size=4, 
                                             shuffle=True, 
                                             collate_fn=collate_fn)
    
    embedding = Embedding(hparams)
    for step, values in enumerate(dataloader):
        texts, _, _, _ = values
        embedding_result = embedding(texts)
        
        print('Embedding result: {}'.format(embedding_result.size()))
        if step == 4:
            break
            
            
    ####
    Embedding result: torch.Size([4, 48, 512])
    Embedding result: torch.Size([4, 48, 512])
    Embedding result: torch.Size([4, 46, 512])
    Embedding result: torch.Size([4, 55, 512])
    Embedding result: torch.Size([4, 58, 512])

     

     

     


    PreNet Layer

     

    PreNet Layer부터가 약간 머리가 아프기 시작합니다.

    사실 알고 보면 별거 없긴 한데,

    이 부분부터 차원을 잘 맞춰야 마지막까지 수월합니다.

     

    PreNet은 Decoder에서 AttentionRNN의 전 단계에서도 사용되기 때문에

    랜덤생성된 Mel shape과 일치하도록 input shape을 조정해줄 필요가 있습니다.

    저는 그 부분을 use_decoder=False로 하여금 조절할 수 있게 하려고 합니다.

     

    그리고 중요한 부분이 하나 있는데요,

    바로 Reduction Facotr에 대한 부분입니다.

    저는 잘 이해가 가지 않는 부분인데, 소리의 연속성을 이용해서

    각 Decoder step에서 만들어지는 Frame을 Reduction Facotr의 배수만큼

    만들게되면 효과가 우수하다고 하네요.

    논문에서는 Reduction Factor를 2로 사용했다고 하는데,

    5도 잘 작동한다고 하니 저도 5를 사용하겠습니다.

    Reduction Factor에 대해 열심히 구글링해서 찾아본 결과로는 대부분 5를 사용하는 듯 합니다. 하지만 Tacotron2에서는 사용되지 않는다고 하니 참고바랍니다.

    Reduction Factor를 사용하게되면, PreNet과 Post CBHG Layer 사용 시

    각 Layer에 대한 Input과 Output shape이 달라지게 되는데요,

    이 점 유의해야 할 것 같습니다.

     

    <아래 언급된 부분에 대한 포스팅은 수정되었습니다.>

    왜냐하면, 앞서 제가 진행했던 Pre-processing 단계에서는

    Reduction Factor에 대한 언급이 없었기 때문에

    Encoder와 Decoder에만 Reduction Factor를 고려해서 구현하고 출력하게되면 

    딱 Reduction Factor의 배수만큼 Original과 Prediction이 차이가 나기 때문에

    결과적으로 Loss를 계산할 수 없게 됩니다.

     

    따라서 다음 포스팅에서 학습 단계를 다룰 때는

    Pre-processing 단계에서 Reduction Factor를 고려한 다음 진행해야할 것 같습니다.

    class PreNet(torch.nn.Module):
        def __init__(self, Hparams, use_decoder=False):
            super(PreNet, self).__init__()
            self.Hparams = Hparams
            
            self.prenet_input_dim = self.Hparams.in_dim
            if use_decoder==True:
                self.prenet_input_dim = self.Hparams.n_mels*self.Hparams.reduction
                
            in_sizes = [self.prenet_input_dim] + [self.Hparams.prenet_linear_size]
            out_sizes = [self.Hparams.prenet_linear_size] + [self.Hparams.prenet_linear_size//2]
    
            layers = []
            for (in_features, out_features) in zip(in_sizes, out_sizes):
                layers += [CustomLinear(in_features=in_features, 
                                        out_features=out_features,
                                        activation='relu',
                                        dropout=self.Hparams.prenet_dropout_ratio)]
    
            self.prenet = torch.nn.Sequential(*layers)
    
        def forward(self, x):
            return self.prenet(x)
    dataset = KSSDataset(hparams)
    
    dataloader = torch.utils.data.DataLoader(dataset, 
                                             batch_size=4, 
                                             shuffle=True, 
                                             collate_fn=collate_fn)
    
    embedding = Embedding(hparams)
    prenet = PreNet(hparams)
    dec_prenet = PreNet(hparams, use_decoder=True)
    for step, values in enumerate(dataloader):
        texts, _, _, _ = values
        embedding_result = embedding(texts)
        prenet_result = prenet(embedding_result)
        
        print('Embedding result: {} \nPreNet result: {} \n'.format(
            embedding_result.size(), prenet_result.size()))
        if step == 4:
            break
            
    
    
    ####
    Embedding result: torch.Size([4, 67, 512]) 
    PreNet result: torch.Size([4, 67, 128]) 
    
    Embedding result: torch.Size([4, 38, 512]) 
    PreNet result: torch.Size([4, 38, 128]) 
    
    Embedding result: torch.Size([4, 72, 512]) 
    PreNet result: torch.Size([4, 72, 128]) 
    
    Embedding result: torch.Size([4, 55, 512]) 
    PreNet result: torch.Size([4, 55, 128]) 
    
    Embedding result: torch.Size([4, 49, 512]) 
    PreNet result: torch.Size([4, 49, 128])

    Hparams에서는 prenet_linear_size를 258로 지정했는데 결과는 128로 나왔습니다.

    이유는 다음과 같습니다.

    in_sizes = [self.Hparams.in_dim] + [self.Hparams.prenet_linear_size]
    out_sizes = [self.Hparams.prenet_linear_size] + [self.Hparams.prenet_linear_size//2]

    위의 코드블럭을 보면 out_sizes 리스트의

    두 번째 값으로 들어오는 값은 prenet_linear_size//2 이기 때문에

    output shape가 256//2=128로 지정되었기 때문입니다.

     

    굳이 이렇게 안 해도 괜찮지만 특정 숫자를 넣게 된다면

    다음 Layer인 CBHG Layer의 첫 input shape도 알맞게 수정해야 해서

    조금 귀찮은 부분이 있었습니다.

    따라서 저는 CBHG의 input shape를 PreNet과 자연스럽게 연결하기 위해

    위와 같이 했다고 보시면 되겠습니다.

     

    in_dim(embedding_dim) 값과 연결해볼 수도 있겠네요.

    다음에 한번 시도해봐야겠습니다.

     

    다음은 제가 사전에 정의한 Custom Linear Layer입니다.

    아래의 이미지를 보면,

    FC-256-ReLU -> Dropout(0.5) -> FC-128-ReLU -> Dropout(0.5)

    처럼 되어있는 걸 볼 수 있습니다.

    torch.nn.Linear() -> torch.nn.ReLU() -> torch.nn.Dropout() 순서이므로 

    Custom Linear Layer의 activation 인자 값에

    원하는 activation function을 입력하시면 됩니다.

     

    하지만 논문에서는 activation function을 ReLU function을 사용했으므로

    저는 ReLU function을 입력하기 위해 'relu'를 지정했습니다.

     

    그리고 Custom Linear Layer 코드 내부에서

    dropout 인자에 0에서 1 사이의 숫자가 들어오게 되면

    torch.nn.Dropout() Layer가 동작하게끔 설계했으므로

    원하는 dropout ratio를 입력하시면 됩니다.

     

    또한 논문에서는 dropout ratio 값을 0.5로 지정했으므로

    저 또한 0.5로 지정했습니다.

     

    Decoder에서 사용되는 PreNet은 Decoder를 다루는 포스팅에서

    출력해보도록 하겠습니다.

     

     

     


    CBHG

    논문 3 페이지에 있는 CBHG 구조 이미지 입니다.

     

    Encoder 부분에서 가장 코드가 긴 부분입니다.

    논문을 보면서 코드를 작성하긴 했지만, 아직도 부족한 부분이 보이는 듯합니다.

     

    Tacotron2에서는 CBHG 부분은 아예 없기도 하고,

    Tacotron1과 Vocoder를 Melgan으로 사용하면 Post CBHG는 필요 없다고 합니다.

    구현을 하긴 하지만 비중 있게 다루지는 않겠습니다.

     

    Encoder에 사용되는 CBHG와 

    Decoder 다음에 사용될 Post CBHG는

    코드 내부에서 지정되는 인자 값에 차이가 있습니다.

     

    아래 두 개의 이미지를 보시면 알 수 있듯이

    Conv1D bank에 사용되는 K

    Conv1D projections에 사용되는 features target features

    위 세 개의 값에 차이가 있습니다.

    K = 16, features = 128, target features = 128 Hparams에서 prenet_linear_size 값을 통해 CBHG 내부 코드를 따라가다보면 Encoder CBHG에 해당하는 값들은 논문 Table 1 에서 지정한 값들과 동일합니다. (embedding_dim 제외)

     

    K = 8, features = 256, target features = 80

     

     

     

    저는 Post CBHG를 구현하기 위해

    아래의 코드블럭과 같이 post=False를 추가했습니다.

    def __init__(self, Hparams, use_post=False):

    구현된 CBHG 코드를 보면 if self.use_post== True: 부분을 볼 수 있을 텐데,

    주석으로 달아놓은 부분을 따라서 보다 보면

    Post CBHG 사용 시 Mel shape와 강제로 맞추기 위한 부분임을 알 수 있습니다.

    (다음 설명은 코드블럭 아래에 있습니다.)

    class CBHG(torch.nn.Module):
        def __init__(self, Hparams, use_post=False):
            super(CBHG, self).__init__()
            self.Hparams = Hparams
            self.use_post = use_post
            
            
            self.conv1d_bank_channels = self.Hparams.prenet_linear_size//2 # Conv1D bank
            self.target_dim = self.conv1d_bank_channels # Conv1D projections
            self.gru_hidden_dim = self.Hparams.cbhg_gru_hidden_dim # Bidirectional GRU
            
            if self.use_post == True:
                self.conv1d_bank_channels = self.Hparams.n_mels # Conv1D bank
                self.Hparams.cbhg_K = self.Hparams.cbhg_K //2 # Conv1D bank
                self.Hparams.cbhg_conv_proj_size = self.Hparams.cbhg_conv_proj_size * 2 # Conv1D projections
                self.gru_hidden_dim = self.Hparams.n_mels # Bidirectional GRU
    
            # For Residual connection
            residual_layers = [torch.nn.Identity()]
                
            # Conv1D bank
            conv1d_bank_layers = []
            for k in range(1, self.Hparams.cbhg_K+1):
                conv1d_bank_layers += [BatchNormConv1D(in_channels=self.conv1d_bank_channels, 
                                                       out_channels=self.conv1d_bank_channels,
                                                       kernel_size=k,
                                                       padding=self.Hparams.cbhg_K//2,
                                                       activation='relu')]
    
            # Conv1D projections
            conv1d_proj_layers = []
    
            conv1d_proj_layers += [torch.nn.MaxPool1d(kernel_size=self.Hparams.cbhg_mp_k, 
                                                      stride=self.Hparams.cbhg_mp_s, 
                                                      padding=self.Hparams.cbhg_mp_p, 
                                                      dilation=self.Hparams.cbhg_mp_d)]
    
            in_sizes = [self.Hparams.cbhg_K * self.conv1d_bank_channels] + [self.Hparams.cbhg_conv_proj_size]
            out_sizes = [self.Hparams.cbhg_conv_proj_size] + [self.conv1d_bank_channels]
            
            for idx, (in_size, out_size) in enumerate(zip(in_sizes, out_sizes)):
                if idx != len(in_sizes)-1:
                    activation = 'relu'
                elif idx == len(in_sizes)-1:
                    activation = None
                
                conv1d_proj_layers += [BatchNormConv1D(in_channels=in_size, 
                                                       out_channels=out_size,
                                                       kernel_size=self.Hparams.cbhg_conv_proj_k,
                                                       padding=self.Hparams.cbhg_conv_proj_p,
                                                       activation=activation)]
            
            pre_highway_layers = [CustomLinear(in_features=out_sizes[-1],
                                               out_features=self.target_dim,
                                               activation=None,
                                               dropout=None)]
            
            # Highway Layers
            highway_layers = []
            for _ in range(4):
                highway_layers += [Highway(in_features=self.target_dim,
                                           out_features=self.target_dim,
                                           activations=['relu', 'sigmoid'],
                                           dropout=None)]
    
            # Bidirectional GRU
            gru_layers = [torch.nn.GRU(input_size=self.target_dim,
                                       hidden_size=self.gru_hidden_dim,
                                       batch_first=True,
                                       bidirectional=True)]
    
            self.residual = torch.nn.Sequential(*residual_layers)
            self.conv1d_bank = torch.nn.ModuleList(conv1d_bank_layers)
            self.conv1d_proj = torch.nn.Sequential(*conv1d_proj_layers)
            self.pre_highway = torch.nn.Sequential(*pre_highway_layers)
            self.highway = torch.nn.Sequential(*highway_layers)
            self.gru = torch.nn.Sequential(*gru_layers)
    
        def forward(self, x):
            short_cut = self.residual(x)
            x = x.permute(0,2,1) # convert to new shape (batch_size, in_dim, T_dim)
            T = x.size(-1)
            x = torch.cat([layer(x)[:,:,:T] for layer in self.conv1d_bank], dim=1) # (B, in_dim, T_in)
            x = self.conv1d_proj(x)
            x = x.permute(0,2,1) # return to origin shape (batch_size, T_dim, in_dim)
            x = x + short_cut
            x = self.pre_highway(x)
            x = self.highway(x)
            x, _ = self.gru(x)
            return x

    아래의 코드블럭은 CBHG 구조에서 Conv1D bank + stacking에 해당하는 부분과

    제가 별도로 지정한 Residual Connection 부분입니다.

    Residual Connection은 forward 부분에서 input 이름을 잘 지정하여

    forward 단에서 처리해도 상관없지만 저는 그냥 Layer를 정의해서 처리했습니다.

     

    제가 사전에 정의한 BatchNormConv1D를 통해 지정된 cbhg_K 값으로 1부터 cbhg_k까지

    각기 다른 kernel_size와 padding 값으로 이루어진 Conv1D Layer가 생성될 것입니다.

     

    Conv1D를 사용할 때는 원하는 shape으로 통과하게 해야 할 텐데,

    앞서 제가 만든 데이터의 shape은 Conv1D에 지정한 차원과 일치하지 않습니다.

    따라서 permute() 함수를 통해 데이터의 shape을 변경합니다.

     

    아무리 Conv1D의 output shape을 잘 맞춘다고 하더라도

    우리가 예상한 것과 다를 수도 있으므로 T 값을 지정하여

    Conv1D를 통과하는 데이터의 shape을 맞출 수 있도록 합니다.

     

    그리고 Conv1D Layer를 통과하면서 달라진 shape를 T 값을 통해 걸러내고

    모든 값들을 리스트에 담게 됩니다.

    마지막으로 torch.cat() 함수를 통해 데이터를

    원하는 shape(dim=1)을 기준으로 concatenate 합니다.

    __init__(self, Hparams, use_post=False):
    
        # For Residual connection
        residual_layers = [torch.nn.Identity()]
    
        # Conv1D bank
        conv1d_bank_layers = []
        for k in range(1, self.Hparams.cbhg_K+1):
            conv1d_bank_layers += [BatchNormConv1D(in_channels=self.conv1d_bank_channels, 
                                                   out_channels=self.conv1d_bank_channels,
                                                   kernel_size=k,
                                                   padding=self.Hparams.cbhg_K//2,
                                                   activation='relu')]
    
        self.conv1d_bank = torch.nn.ModuleList(conv1d_bank_layers)
    
    forward(self, x):
        short_cut = self.residual(x)
        x = x.permute(0,2,1)
        T = x.size(-1)
        x = torch.cat([layer(x)[:,:,:T] for layer in self.conv1d_bank], dim=1) # (B, in_dim, T_in)

     

    그다음 부분은 Conv1D projections 부분입니다.

     

    MaxPool1d 부분은 굳이 padding과 dilation을 해야 하는지 모르겠습니다.

    포스팅한 후에 조금 실험을 해봐서 수정을 하던지 해야겠습니다.

     

    in_sizes, out_sizes는 PreNet Layer와 동일한 방법으로 차원을 맞춥니다.

    앞 단에서 torch.cat() 함수를 이용해

    cbhg_K 개수만큼의 Layer가 dim=1을 기준으로 concatenate 됐기 때문에

    마지막 shape는 상당히 큰 값으로 변해있을 겁니다.

    그 값은 self.Hparams.cbhg_K * self.target_dim 개수 만큼 될 것이므로

    in_sizes 리스트의 첫 값으로 지정해줍니다.

     

    그다음, 아래의 이미지에서 Conv1D projection Layer 사이에 ReLU function은 

    가운데 부분에만 존재해야 하므로

    if elif 문을 사용해서 두 번째 인덱스(두 번째 Layer가 될 때)는

    ReLU function이 들어가지 않게 만들어줍니다.

     

    마지막으로 Linear Layer 부분이 있는데,

    이 부분은 pre_highway_layers를 이름으로

    제가 사전에 정의한 CustomLinear로 하여금 다음 GRU Layer에 입력되는 차원에 맞도록

    조절하게 합니다.

    Residual Connection 부분은 바로 직전의 코드블럭을 참고해주시고

    CBHG 구조에서 Residual Connection이 발생하는 지점 또한 확인하면 되겠습니다.

    Max pooling, Conv1D projections, Linear Layer 

    __init__(self, Hparams, use_post=False):
    
        # Conv1D projections
        conv1d_proj_layers = []
    
        conv1d_proj_layers += [torch.nn.MaxPool1d(kernel_size=self.Hparams.cbhg_mp_k, 
                                                  stride=self.Hparams.cbhg_mp_s, 
                                                  padding=self.Hparams.cbhg_mp_p, 
                                                  dilation=self.Hparams.cbhg_mp_d)]
    
        in_sizes = [self.Hparams.cbhg_K * self.conv1d_bank_channels] + [self.Hparams.cbhg_conv_proj_size]
        out_sizes = [self.Hparams.cbhg_conv_proj_size] + [self.conv1d_bank_channels]
    
        for idx, (in_size, out_size) in enumerate(zip(in_sizes, out_sizes)):
            if idx != len(in_sizes)-1:
                activation = 'relu'
            elif idx == len(in_sizes)-1:
                activation = None
    
            conv1d_proj_layers += [BatchNormConv1D(in_channels=in_size, 
                                                   out_channels=out_size,
                                                   kernel_size=self.Hparams.cbhg_conv_proj_k,
                                                   padding=self.Hparams.cbhg_conv_proj_p,
                                                   activation=activation)]
            
        pre_highway_layers = [CustomLinear(in_features=out_sizes[-1],
                                           out_features=self.target_dim,
                                           activation=None,
                                           dropout=None)]
    
        self.conv1d_proj = torch.nn.Sequential(*conv1d_proj_layers)
        self.pre_highway = torch.nn.Sequential(*pre_highway_layers)
        
    
    forward(self, x):
        short_cut = self.residual(x) # 앞 단의 코드 블럭 참고
        x = self.conv1d_proj(x)
        x = x.permute(0,2,1) # return to origin shape (batch_size, T_dim, in_dim)
        x = x + short_cut # 앞 단의 코드 블럭 참고
        x = self.pre_highway(x)

     

    마지막으로 Highway Layer와 GRU Layer입니다.

    Highway Layer는 높은 퀄리티의 Features를 추출하기 위해 사용된다고 합니다.

    Highway Network에 대해서는 아래의 링크를 참고했습니다.

    링크 내부 포스트 마지막 부분에 관련 논문이 있으니

    관심 있으신 분은 참고하시면 좋을 듯합니다.

    https://lazyer.tistory.com/8

     

    Highway_Network

    highway_network Highway Networks _ Lazyer [arXiv:1505.00387v2] - Rupesh Kumar Srivastava - 03.11.15 Abstract 학습 모델의 깊이가 증가함에 따라 성능이 증가한다는 사실은 일반화되었다. 하지만 깊이가 증..

    lazyer.tistory.com

     

    Highway Layer는 다음과 같이 구현했습니다.

    bias 데이터를 초기화하는 방법은

    LayerName.bias.data.zero_(), LayerName.bias.data.fill_(-1)처럼 할 수 있는데,

    저는 CustomLinear Layer를 별도로 지정했기 때문에

    bias가 직접적으로 있는 부분은 torch.nn.Linear()를 정의한 self.linear입니다.

    따라서 제가 구현한 방식에서 bias를 초기화할 때는 아래와 같이 진행하면 됩니다.

    LayerName.linear.bias.data.zero_(), LayerName.linear.bias.data.fill_(-1)

    class Highway(torch.nn.Module):
        def __init__(self, in_features, out_features, activations=['relu', 'sigmoid'], dropout=None):
            super(Highway, self).__init__()
            self.in_features = in_features
            self.out_features = out_features
            self.relu = activations[0]
            self.sigmoid = activations[1]
            self.dropout = dropout
            self.H = CustomLinear(in_features=self.in_features, 
                                  out_features=self.out_features, 
                                  activation=self.relu, 
                                  dropout=self.dropout)
            self.H.linear.bias.data.zero_()
            self.T = CustomLinear(in_features=self.in_features, 
                                  out_features=self.out_features, 
                                  activation=self.sigmoid, 
                                  dropout=self.dropout)
            self.T.linear.bias.data.fill_(-1)
    
        def forward(self, x):
            return self.H(x) * self.T(x) + x * (1.0 - self.T(x))
    highway = Highway(10, 10, activations=['relu', 'sigmoid'], dropout=None)
    
    print(highway)
    
    
    ####
    Highway(
      (H): CustomLinear(
        (linear): Linear(in_features=10, out_features=10, bias=True)
        (activation): ReLU()
      )
      (T): CustomLinear(
        (linear): Linear(in_features=10, out_features=10, bias=True)
        (activation): Sigmoid()
      )
    )

    아래의 이미지에서 볼 수 있듯이 Highway Layer를 4개 추가합니다.

     

    GRU Layer는 Birectional GRU Layer입니다.

    결론적으로 target_dim으로 원하는 값으로 설정한다 한들, 

    결과값은 target_dim보다 두 배 큰 값이 결과값으로 나온다는 말입니다.

     

    따라서 다음에 구현할 Decoder나

    Post CBHG를 지난 Mel이

    Linear-scale Spectrogram으로 변환될 때 사용할 Linear Layer에서

    각각의 input shape을 잘 살펴보아야 할 것입니다.

    Highway, Bidirectional GRU Layer

    __init__(self, Hparams, post=False):
        # Highway Layers
        highway_layers = []
        for _ in range(4):
            highway_layers += [Highway(in_features=self.target_dim,
                                       out_features=self.target_dim,
                                       activations=['relu', 'sigmoid'],
                                       dropout=None)]
    
        # Bidirectional GRU
        gru_layers = [torch.nn.GRU(input_size=self.target_dim,
                                   hidden_size=self.gru_hidden_dim,
                                   batch_first=True,
                                   bidirectional=True)]
    
        self.highway = torch.nn.Sequential(*highway_layers)
        self.gru = torch.nn.Sequential(*gru_layers)
        
    forward(self, x):
        x = self.highway(x)
        x, _ = self.gru(x)

     

     

    구현된 CBHG까지 output shape을 확인해봅니다.

    dataset = KSSDataset(hparams)
    
    dataloader = torch.utils.data.DataLoader(dataset, 
                                             batch_size=4, 
                                             shuffle=True, 
                                             collate_fn=collate_fn)
    
    embedding = Embedding(hparams)
    prenet = PreNet(hparams, use_decoder=False)
    cbhg = CBHG(hparams, use_post=False)
    for step, values in enumerate(dataloader):
        texts, mels, _, _ = values
        embedding_result = embedding(texts)
        prenet_result = prenet(embedding_result)
        cbhg_result = cbhg(prenet_result)
        
        print('Embedding result: {} \nPreNet result: {} \nCBHG result: {} \n'.format(
            embedding_result.size(), prenet_result.size(), cbhg_result.size()))
        if step == 4:
            break
            
            
            
            
    ####
    Embedding result: torch.Size([4, 42, 512]) 
    PreNet result: torch.Size([4, 42, 128]) 
    CBHG result: torch.Size([4, 42, 256]) 
    
    Embedding result: torch.Size([4, 50, 512]) 
    PreNet result: torch.Size([4, 50, 128]) 
    CBHG result: torch.Size([4, 50, 256]) 
    
    Embedding result: torch.Size([4, 52, 512]) 
    PreNet result: torch.Size([4, 52, 128]) 
    CBHG result: torch.Size([4, 52, 256]) 
    
    Embedding result: torch.Size([4, 48, 512]) 
    PreNet result: torch.Size([4, 48, 128]) 
    CBHG result: torch.Size([4, 48, 256]) 
    
    Embedding result: torch.Size([4, 51, 512]) 
    PreNet result: torch.Size([4, 51, 128]) 
    CBHG result: torch.Size([4, 51, 256])

    예상한 대로 CBHG의 output shape은

    Bidirectional GRU의 영향 때문에 output shape은 2배가 됐습니다.

     

    마지막으로 Encoder를 각 Layer 별로 묶은 뒤 데이터 별 output shape과 

    Encoder의 모델 구조를 확인해보고 포스팅을 마치겠습니다.

     


    Encoder

     

    class Encoder(torch.nn.Module):
        def __init__(self, Hparams, use_decoder=False, use_post=False):
            super(Encoder, self).__init__()
            self.Hparams = Hparams
            self.use_decoder = use_decoder
            self.use_post = use_post
            
            layers = [Embedding(self.Hparams),
                      PreNet(self.Hparams, use_decoder=self.use_decoder),
                      CBHG(self.Hparams, use_post=self.use_post)]
    
            self.encoder = torch.nn.Sequential(*layers)
    
        def forward(self, x):
            return self.encoder(x)
    dataset = KSSDataset(hparams)
    
    dataloader = torch.utils.data.DataLoader(dataset, 
                                             batch_size=4, 
                                             shuffle=True, 
                                             collate_fn=collate_fn)
    
    embedding = Embedding(hparams)
    prenet = PreNet(hparams, use_decoder=False)
    cbhg = CBHG(hparams, use_post=False)
    encoder = Encoder(hparams, use_decoder=False, use_post=False)
    for step, values in enumerate(dataloader):
        texts, mels, _, _ = values
        
        embedding_result = embedding(texts)
        prenet_result = prenet(embedding_result)
        cbhg_result = cbhg(prenet_result)
        encoded = encoder(texts)
        
        print('Embedding result: {} \nPreNet result: {} \nCBHG result: {} \nEncoder result: {} \n'.format(
            embedding_result.size(), prenet_result.size(), cbhg_result.size(), encoded.size()))
        if step == 4:
            break
            
            
    ####
    Embedding result: torch.Size([4, 50, 512]) 
    PreNet result: torch.Size([4, 50, 128]) 
    CBHG result: torch.Size([4, 50, 256]) 
    Encoder result: torch.Size([4, 50, 256]) 
    
    Embedding result: torch.Size([4, 50, 512]) 
    PreNet result: torch.Size([4, 50, 128]) 
    CBHG result: torch.Size([4, 50, 256]) 
    Encoder result: torch.Size([4, 50, 256]) 
    
    Embedding result: torch.Size([4, 47, 512]) 
    PreNet result: torch.Size([4, 47, 128]) 
    CBHG result: torch.Size([4, 47, 256]) 
    Encoder result: torch.Size([4, 47, 256]) 
    
    Embedding result: torch.Size([4, 72, 512]) 
    PreNet result: torch.Size([4, 72, 128]) 
    CBHG result: torch.Size([4, 72, 256]) 
    Encoder result: torch.Size([4, 72, 256]) 
    
    Embedding result: torch.Size([4, 42, 512]) 
    PreNet result: torch.Size([4, 42, 128]) 
    CBHG result: torch.Size([4, 42, 256]) 
    Encoder result: torch.Size([4, 42, 256])
    encoder = Encoder(hparams, use_decoder=False, use_post=False)
    
    print(encoder)
    
    
    ####
    Encoder(
      (encoder): Sequential(
        (0): Embedding(
          (embedding_layer): Sequential(
            (0): Embedding(74, 512)
          )
        )
        (1): PreNet(
          (prenet): Sequential(
            (0): CustomLinear(
              (linear): Linear(in_features=512, out_features=256, bias=True)
              (activation): ReLU()
              (dropout): Dropout(p=0.5, inplace=False)
            )
            (1): CustomLinear(
              (linear): Linear(in_features=256, out_features=128, bias=True)
              (activation): ReLU()
              (dropout): Dropout(p=0.5, inplace=False)
            )
          )
        )
        (2): CBHG(
          (residual): Sequential(
            (0): Identity()
          )
          (conv1d_bank): ModuleList(
            (0): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(1,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (1): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(2,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (2): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(3,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (3): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(4,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (4): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(5,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (5): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(6,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (6): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(7,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (7): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(8,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (8): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(9,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (9): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(10,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (10): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(11,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (11): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(12,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (12): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(13,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (13): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(14,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (14): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(15,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (15): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(16,), stride=(1,), padding=(8,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
          )
          (conv1d_proj): Sequential(
            (0): MaxPool1d(kernel_size=2, stride=1, padding=1, dilation=2, ceil_mode=False)
            (1): BatchNormConv1D(
              (conv1d): Conv1d(2048, 128, kernel_size=(3,), stride=(1,), padding=(1,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
              (activation): ReLU()
            )
            (2): BatchNormConv1D(
              (conv1d): Conv1d(128, 128, kernel_size=(3,), stride=(1,), padding=(1,))
              (batchnorm1d): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
            )
          )
          (pre_highway): Sequential(
            (0): CustomLinear(
              (linear): Linear(in_features=128, out_features=128, bias=True)
            )
          )
          (highway): Sequential(
            (0): Highway(
              (H): CustomLinear(
                (linear): Linear(in_features=128, out_features=128, bias=True)
                (activation): ReLU()
              )
              (T): CustomLinear(
                (linear): Linear(in_features=128, out_features=128, bias=True)
                (activation): Sigmoid()
              )
            )
            (1): Highway(
              (H): CustomLinear(
                (linear): Linear(in_features=128, out_features=128, bias=True)
                (activation): ReLU()
              )
              (T): CustomLinear(
                (linear): Linear(in_features=128, out_features=128, bias=True)
                (activation): Sigmoid()
              )
            )
            (2): Highway(
              (H): CustomLinear(
                (linear): Linear(in_features=128, out_features=128, bias=True)
                (activation): ReLU()
              )
              (T): CustomLinear(
                (linear): Linear(in_features=128, out_features=128, bias=True)
                (activation): Sigmoid()
              )
            )
            (3): Highway(
              (H): CustomLinear(
                (linear): Linear(in_features=128, out_features=128, bias=True)
                (activation): ReLU()
              )
              (T): CustomLinear(
                (linear): Linear(in_features=128, out_features=128, bias=True)
                (activation): Sigmoid()
              )
            )
          )
          (gru): Sequential(
            (0): GRU(128, 128, batch_first=True, bidirectional=True)
          )
        )
      )
    )

     

     

    상당히 긴 포스팅이었습니다.

     

    제가 느끼기에 Encoder는 Decoder보다 훨씬 쉽다고 여겼는데,

    앞으로 구현해야 하는 Decoder를 포스팅할 때 느낄 고통이 간접적으로 느껴지네요.

     

    다음 포스팅에는 Attention과 더불어 Decoder와 Post CBHG

    그리고 Linear-scale Spectrogram까지 출력하는 코드를 구현해오겠습니다.

     

    긴 글 읽어주셔서 감사합니다.

     

     

    제가 작성한 포스트에 오류가 있다면 언제든지 댓글 부탁드립니다 :)

    'Tacotron 1' 카테고리의 다른 글

    Tacotron 무지성 구현 - 6/N  (0) 2021.08.02
    Tacotron 무지성 구현 - 5/N  (0) 2021.07.30
    Tacotron 무지성 구현 - 3/N  (0) 2021.07.27
    Tacotron 무지성 구현 - 2/N  (0) 2021.07.27
    Tacotron 무지성 구현 - 1/N  (0) 2021.07.27

    댓글

Designed by Tistory.