ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Tacotron 무지성 구현 - 2/N
    Tacotron 1 2021. 7. 27. 13:33

    이전 포스팅에서 오디오 데이터를 Spectrogram과 Mel-Spectrogram으로 변환하는 방법까지 살펴보았습니다.

     

    제가 포스팅하면서 모니터 한켠에 주피터 노트북을 띄어두고 코드를 작성했는데,

    작성하다보니 좀 이상한 부분이 있었네요.

    다음 코드 블럭에서 수정한 부분을 짚고 넘어갈테니 참고 바랍니다.


    Hyper Parameters

     

    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

     

    import torch, torchaudio
    
    
    class Spectrogram(torch.nn.Module):
        def __init__(self, Hparams):
            super(Spectrogram, self).__init__()
            self.Hparams = Hparams
            self.resampler = torchaudio.transforms.Resample(orig_freq=self.Hparams.origin_sample_rate,
                                                            new_freq=self.Hparams.sample_rate)
            self.mel_filter = torchaudio.transforms.MelScale(n_mels=self.Hparams.n_mels,
                                                             sample_rate=self.Hparams.sample_rate)
    
        def forward(self, audio, mel_return=True):
            if audio.size(0) != 1: # if stereo
                audio = torch.mean(audio, dim=0).unsqueeze(0) # to mono
            
            audio = self.resampler(audio)
            
            stft = torch.stft(input=audio, 
                              n_fft=self.Hparams.n_fft, 
                              hop_length=self.Hparams.hop_length, 
                              win_length=self.Hparams.win_length, 
                              return_complex = True)
    
            D = torch.abs(stft)
            min_level = torch.exp(self.Hparams.min_level_db / 20 * torch.log(torch.Tensor([10])))
            spec = 20 * torch.log(torch.maximum(min_level, D)) - self.Hparams.ref_level_db
            spec = spec.permute(0,2,1) # (1, t_seq, spec_dim)
            
            if mel_return:
                filtered_D = self.mel_filter(D)
                mel = 20 * torch.log(torch.maximum(min_level, filtered_D)) - self.Hparams.ref_level_db
                mel = mel.permute(0,2,1) # (1, t_seq, n_mels)
            else:
                mel = None
    
            return spec, mel

     

    모델을 구축할 때 사용하는 Forward Propagation에 해당하는 forward 함수 부분을 작성해서

    오디오를 전처리한 부분입니다.

    제가 봐도 좀 아닌 것 같아서 다음과 같이 수정했습니다.

     

    import torch, torchaudio
    
    
    class AudioPreprocessing():
        def __init__(self, Hparams):
            self.Hparams = Hparams
            self.resampler = torchaudio.transforms.Resample(orig_freq=self.Hparams.origin_sample_rate,
                                                            new_freq=self.Hparams.sample_rate)
            self.mel_filter = torchaudio.transforms.MelScale(n_mels=self.Hparams.n_mels,
                                                             sample_rate=self.Hparams.sample_rate)
    
        def spectrogram(self, audio):
            # 오디오 채널이 모노가 아닐 때 모노로 변환
            if audio.size(0) !=1:
                audio = torch.mean(audio, dim=0).unsqueeze(0)
            
            # 오디오 샘플레이트가 원하는 샘플레이트가 아닐 때 리샘플링하는 부분
            audio = self.resampler(audio)
            
            stft = torch.stft(input=audio, 
                              n_fft=self.Hparams.n_fft, 
                              hop_length=self.Hparams.hop_length, 
                              win_length=self.Hparams.win_length, 
                              return_complex = True)
    
            D = torch.abs(stft)
            min_level = torch.exp(self.Hparams.min_level_db / 20 * torch.log(torch.Tensor([10])))
            spec = 20 * torch.log(torch.maximum(min_level, D)) - self.Hparams.ref_level_db
            spec = spec.permute(0,2,1).squeeze(0)
            return spec
            
        def melspectrogram(self, audio):
            if audio.size(0) != 1:
                audio = torch.mean(audio, dim=0).unsqueeze(0)
            
            audio = self.resampler(audio)
            
            stft = torch.stft(input=audio, 
                              n_fft=self.Hparams.n_fft, 
                              hop_length=self.Hparams.hop_length, 
                              win_length=self.Hparams.win_length, 
                              return_complex = True)
    
            D = torch.abs(stft)
            min_level = torch.exp(self.Hparams.min_level_db / 20 * torch.log(torch.Tensor([10])))
            filtered_D = self.mel_filter(D)
            mel = 20 * torch.log(torch.maximum(min_level, filtered_D)) - self.Hparams.ref_level_db
            mel = mel.permute(0,2,1).squeeze(0)
            return mel

     

    오디오 관련 신호처리 코드를 하나의 클래스에 묶어서 관리하면 편리할 것 같아 보입니다.

    이전 포스팅에서 언급한  torchaudio.transforms.Spectrogram과 torchaudio.transforms.MelSpectrogram을

    이용해서 구해도 상관없습니다.

    아래의 링크를 참고하시면 도움이 될 것 같아요.

    https://pytorch.org/audio/stable/transforms.html

     

    torchaudio.transforms — Torchaudio 0.9.0 documentation

    torchaudio.transforms Transforms are common audio transforms. They can be chained together using torch.nn.Sequential Spectrogram class torchaudio.transforms.Spectrogram(n_fft: int = 400, win_length: Optional[int] = None, hop_length: Optional[int] = None, p

    pytorch.org

     

    새로 작성한 클래스를 이용하여 Spectrogram과 Mel-Spectrogram을 출력해서 shape을 살펴보면

    다음과 같습니다.

     

    import torch, torchaudio
    
    
    class AudioPreprocessing():
        def __init__(self, Hparams):
            self.Hparams = Hparams
            self.resampler = torchaudio.transforms.Resample(orig_freq=self.Hparams.origin_sample_rate,
                                                            new_freq=self.Hparams.sample_rate)
            self.mel_filter = torchaudio.transforms.MelScale(n_mels=self.Hparams.n_mels,
                                                             sample_rate=self.Hparams.sample_rate)
    
        def spectrogram(self, audio):
            # 오디오 채널이 모노가 아닐 때 모노로 변환
            if audio.size(0) !=1:
                audio = torch.mean(audio, dim=0).unsqueeze(0)
            
            # 오디오 샘플레이트가 원하는 샘플레이트가 아닐 때 리샘플링하는 부분
            audio = self.resampler(audio)
            
            stft = torch.stft(input=audio, 
                              n_fft=self.Hparams.n_fft, 
                              hop_length=self.Hparams.hop_length, 
                              win_length=self.Hparams.win_length, 
                              return_complex = True)
    
            D = torch.abs(stft)
            min_level = torch.exp(self.Hparams.min_level_db / 20 * torch.log(torch.Tensor([10])))
            spec = 20 * torch.log(torch.maximum(min_level, D)) - self.Hparams.ref_level_db
            spec = spec.permute(0,2,1).squeeze(0)
            return spec
            
        def melspectrogram(self, audio):
            if audio.size(0) != 1:
                audio = torch.mean(audio, dim=0).unsqueeze(0)
            
            audio = self.resampler(audio)
            
            stft = torch.stft(input=audio, 
                              n_fft=self.Hparams.n_fft, 
                              hop_length=self.Hparams.hop_length, 
                              win_length=self.Hparams.win_length, 
                              return_complex = True)
    
            D = torch.abs(stft)
            min_level = torch.exp(self.Hparams.min_level_db / 20 * torch.log(torch.Tensor([10])))
            filtered_D = self.mel_filter(D)
            mel = 20 * torch.log(torch.maximum(min_level, filtered_D)) - self.Hparams.ref_level_db
            mel = mel.permute(0,2,1).squeeze(0)
            return mel
            
            
    hparams = Hparams()
    ap = AudioPreprocessing(hparams)
    
    
    for idx, (path, _) in enumerate(alignments.items()):
        audio, _ = torchaudio.load(path)
        spec = ap.spectrogram(audio)
        mel = ap.melspectrogram(audio)
        print(spec.size(), mel.size())
        if idx==4:
            break
            
    
    torch.Size([608, 513]) torch.Size([608, 80])
    torch.Size([685, 513]) torch.Size([685, 80])
    torch.Size([304, 513]) torch.Size([304, 80])
    torch.Size([398, 513]) torch.Size([398, 80])
    torch.Size([230, 513]) torch.Size([230, 80])

    AudioPreprocessing 메서드에서 각 값의 첫 번째 차원을 없앤 것을 제외하면

    이전 포스팅에서 본 결과값과 동일하죠?

    어떤 방법을 사용하던지 상관없습니다.

    다만 끝 부분의 shape는 동일한지 잘 살펴보아야 합니다.

     

     

     


    텍스트 데이터 전처리

     

    텍스트(문장) 데이터는 특수문자 사용 여부에 따라서 처리하는 방법이 달라집니다.

    한글이 아닌 문자를 한글로 변환해주거나, 기호 또는 단위 앞에 오는 숫자들의 발음이 달라지는 것처럼

    전처리 방법 자체가 상당히 복잡하기 때문에 저처럼 초심자인 분들은

    타코트론을 공부하려고 왔다가 자연어처리 부분을 더 공부하게되는 신기한 현상을 마주하게 됩니다.

    따라서 저는 github에서 흔히 볼 수 있는(이미 널리 쓰이고 있는) 한국어 전처리 코드를 가져와서

    사용하도록 하겠습니다.

     

     

    사용할 코드는 한국어를 유니코드로 사용하여 초성, 중성, 종성으로 구분하여 사용할 수 있게끔 하고,

    초성, 중성, 종성으로 분리해주는 토크나이저 등등 앞으로 구현할 Encoder의 Embedding Layer 부분에

    입력될 Sequence로 변환해주는 함수입니다.

    from jamo import hangul_to_jamo
    
    
    # symbol 정리
    PAD = '_'
    EOS = '~'
    SPACE = ' '
    
    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
    
    
    # text_to_sequence() 함수에 사용되는 dict, 전처리 할 때 사용됨
    _symbol_to_id = {s: i for i, s in enumerate(symbols)}
    
    # sequence_to_text() 함수에 사용되는 dict, 전처리 할 때 사용안함
    _id_to_symbol = {i: s for i, s in enumerate(symbols)}
    
    
    # 텍스트 데이터를 전처리 할 때 사용되는 함수
    def text_to_sequence(text):
        sequence = []
        if not 0x1100 <= ord(text[0]) <= 0x1113:
            text = ''.join(list(hangul_to_jamo(text)))
        for s in text:
            sequence.append(_symbol_to_id[s])
        sequence.append(_symbol_to_id['~'])
        return sequence
    
    
    # 나중에 사용됨
    def sequence_to_text(sequence):
        result = ''
        for symbol_id in sequence:
            if symbol_id in _id_to_symbol:
                s = _id_to_symbol[symbol_id]
                result += s
        return result.replace('}{', ' ')

     

    구글에 "Tacotron 한국어 전처리" 라고 검색만 해도 흔히 볼 수 있는 코드입니다.

    그 중에서 chldkato 님의 코드를 빌려왔습니다.

    복잡한 부분은 최대한 생략했고, 바로 학습하는데 사용할 수 있는 부분만 옮겨왔습니다.

     

    코드를 살펴보면 PAD, EOS, SPACE 등 여러 문자열이 정의되어있습니다.

     

    PAD는, 나중에 torch.utils.data.DataLoader()에서 별도의 collate_fn 함수를 작성하여 함수 내부에

    torch.nn.utils.rnn.pad_sequence 함수를 입력하게 됩니다.

    그 때 padding되는 값을 0으로 지정하면,

    _symbol_to_id에서 맨 처음에 입력되는 값이 PAD일 때 PAD의 id 값이 0으로 지정되므로,

    사전에 정의한 PAD의 문자열이 자연스럽게 0과 매핑되게끔 할 수 있습니다.

     

    EOS는 문자열이 끝나는 것임을 지정하는 문자열입니다.

    흔히 End Of Sentence 의 약자라고 합니다.

     

    Space는 공백입니다.

     

    이 외에 다른 문자열을 지정하고 싶으면 해도 상관은 없을 테지만, 그만큼 복잡해지므로

    간단한 구현을 위해서는 추천하지 않습니다.

     

    JAMO_LEADS는 한글에서 초성에 해당하는 부분이며 유니코드로 표현됩니다.

    JAMO_VOWELS는 한글에서 중성에 해당하는 부분이며 유니코드로 표현됩니다.

    JAMO_TAILS는 한글에서 종성에 해당하는 부분이며 유니코드로 표현됩니다.

     

    문자열을 모두 이어붙여서 symbols라는 변수에 저장합니다.

    symbols를 따로 정의해서 단어사전을 구축하게 되는데,

    나중에 이 symbols의 개수가 torch.nn.Embedding()에서num_embeddings의 입력값이 됩니다.

     

    마지막으로 enumerate()함수를 사용해 _symbol_to_id, _id_to_symbol을 지정합니다.

     

    문자열을 시퀀스로 변환할 때 지정한 문자열 외에 다른 문자열이 있다면 에러가 발생할 수 있습니다.

     

    그럴 때는 re 모듈을 사용해 아래와 같이 특정 문자열을 제거할 수 있습니다.

     

    import re
    
    
    sentence = "안.녕,하!세?요?"
    filters = '([.,!?])'
    cleaner = re.compile(filters)
    new_sentence = re.sub(cleaner, '', sentence)
    
    print(new_sentence)
    
    안녕하세요

     

    저는 특수문자를 지우지 않고 ".,!?" 문자열을 별도로 symbols에 추가한 뒤에 진행하도록 하겠습니다.

    전처리하는 데이터에 원하지 않은 문자열이 있다면 re 모듈을 활용하거나 strip(), replace() 함수 등을 이용해서

    지우는 방법도 좋을 것 같습니다.

     

    from jamo import hangul_to_jamo
    
    
    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)}
    
    
    def text_to_sequence(text):
        sequence = []
        if not 0x1100 <= ord(text[0]) <= 0x1113:
            text = ''.join(list(hangul_to_jamo(text)))
        for s in text:
            sequence.append(_symbol_to_id[s])
        sequence.append(_symbol_to_id['~'])
        return sequence
    
    
    
    # import re
    
    # sentence = "안.녕,하!세?요?"
    # filters = '([.,!?])'
    # cleaner = re.compile(filters)
    # new_sentence = re.sub(cleaner, '', sentence)
    
    # print(new_sentence)
    
    
    
    for idx, (_, text) in enumerate(alignments.items()):
        # text = re.sub(cleaner, '', text) # (cleaner, 변환할 문자, 기존 문장)
        sequence = text_to_sequence(text)
        print("Text: {} \nSequence: {}\n".format(text, sequence))
        if idx==4:
            break
            
            
    
    Text: 그는 괜찮은 척하려고 애쓰는 것 같았다. 
    Sequence: [2, 39, 4, 39, 45, 69, 2, 31, 45, 16, 21, 47, 13, 39, 45, 69, 16, 25, 42, 20, 21, 7, 27, 2, 29, 69, 13, 22, 12, 39, 4, 39, 45, 69, 2, 25, 60, 69, 2, 21, 66, 13, 21, 61, 5, 21, 70, 1]
    
    Text: 그녀의 사랑을 얻기 위해 애썼지만 헛수고였다. 
    Sequence: [2, 39, 4, 27, 13, 40, 69, 11, 21, 7, 21, 62, 13, 39, 49, 69, 13, 25, 48, 2, 41, 69, 13, 37, 20, 22, 69, 13, 22, 12, 25, 61, 14, 41, 8, 21, 45, 69, 20, 25, 60, 11, 34, 2, 29, 13, 27, 61, 5, 21, 70, 1]
    
    Text: 용돈을 아껴 써라. 
    Sequence: [13, 33, 62, 5, 29, 45, 13, 39, 49, 69, 13, 21, 3, 27, 69, 12, 25, 7, 21, 70, 1]
    
    Text: 그는 아내를 많이 아낀다. 
    Sequence: [2, 39, 4, 39, 45, 69, 13, 21, 4, 22, 7, 39, 49, 69, 8, 21, 47, 13, 41, 69, 13, 21, 3, 41, 45, 5, 21, 70, 1]
    
    Text: 그 애 전화번호 알아? 
    Sequence: [2, 39, 69, 13, 22, 69, 14, 25, 45, 20, 30, 9, 25, 45, 20, 29, 69, 13, 21, 49, 13, 21, 73, 1]

     

    결과를 살펴보면, 각 초성, 중성, 종성들이 _symbol_to_id에 지정된 자연수로 지정되었고,

    시퀀스 리스트의 마지막 부분에 1로 지정되어 있는 시퀀스는

    앞서 정의했던 EOS의 '~' 문자열이 _symbol_to_id에서 두번째로 들어갔기 때문에

    enumerate()함수의 index가 1로 지정되면서 EOS의 '~'가 숫자 1과 매핑된 것입니다.

     

    패딩은 아직 진행하지 않았기 때문에 숫자 0은 보이지 않는 것이 당연합니다.

    현재까지 전처리 된 문자열은 one-hot encoding 되었다 라고 합니다.

     

    one-hot encoding에 대해서는 아래의 링크를 참고 바랍니다.

    (딥러닝을 이용한 자연어 처리 입문 one-hot encoding chapter)

    https://wikidocs.net/22647

     

    위키독스

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

    wikidocs.net

     

    이번 포스팅에서는 오디오 5개, 텍스트 5개를 예시로 해서 간단한 전처리를 해보았습니다.

    한글을 유니코드로 symbols에 지정하는 부분을 제외하면

    jamo 라이브러리를 활용해 간단하게 구현할 수 있었습니다.

    각 과정에서 작업자가 원하는 방법 도입해 전처리를 진행할 수도 있습니다.

     

     

    아직 공부하고 있지만 공부하면서 어려웠던 점은

    Tacotron 모델 구조도 나름대로 어렵지만,

    그에 상응하게끔 데이터를 손질하는 방법이 가장 어려웠습니다.

    아마도 저와 같은 기분을 느낀 분들이 많지 않을까 생각합니다.

    (사실 아직 text_to_sequence() 함수 내부에 if not 부분은 이해가 가질 않습니다... 누구 아시는 분,,?)

     

     

    다음 포스팅에서는 오디오, 텍스트 데이터를 전처리하면서 저장하는 방법과

    Custom Dataset, DataLoader 코드를 작성하는 방법을 알아보겠습니다.

     

     

     

     

    저는 아직 초심자입니다.

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

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

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

    댓글

Designed by Tistory.