ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Tacotron 무지성 구현 - 3/N
    Tacotron 1 2021. 7. 27. 22:03

    이전 두 개의 포스팅에서 오디오와 텍스트 전처리하는 코드를 살펴봤습니다.

    이번 포스팅에서는 두 종류의 데이터를 전처리하면서 원하는 경로에 저장하는 코드를 추가해

    불필요한 시간을 줄이고 학습에 바로 사용가능한 형태로 만들어보겠습니다.

     

     

     


    Hyper parameters

     

    지금까지 사용한 인자들을 모두 다음과 같이 Hparams 클래스에 입력합니다.

    이렇게 묶어두면 흔히 볼 수 있는 hparams.py 파일처럼 만들었을 때 불러오기도 편할 것 같습니다.

    모델 구성이나 학습 과정을 설계할 때 필요한 인자들도 여기에 입력하면 됩니다.

    import os
    
    
    class Hparams():
        
        # Audio Pre-processing
        origin_sample_rate = 22050
        sample_rate = 22050
        n_fft = 1024
        hop_length = 256
        win_length = 1024
        n_mels = 80
        reduction = 5
        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')

     

     

     


    Audio Pre-processing

     

    오디오 데이터 전처리에 사용한 클래스는 아래와 같습니다.

    많이 봐서 그런지 이제 지겨울 지경이네요.

    Hparams에 Reduction Factor가 추가됨에따라,

    AudioPreprocessing에 pre_padding 메서드가 추가되었습니다.

    Pre-padding을 진행하게되면 5의 배수에 맞게 Zero-padding이 됩니다.

    (ex: 279->300, 126->130 etc...)

    나중에 실험적으로 느낀 부분이지만, reduction factor를 1로 두고 학습하게 된다면

    굉장히 오랜 시간동안 인내하며 학습되는 걸 지켜봐야 합니다.

    특히 tacotron2에서는 reduction factor를 고려해서 코드가 짜여져 있지만

    실제로 대부분 reduction factor를 1로 두고 학습하기 때문에 

    굉장히 오랜 시간동안 학습이 진행됩니다. 

    (tacotron1 기준 1step 당 약 0.3~0.5초 , tacotron2 기준1step 당 1초~1.7초)

    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
        
        def pre_padding(self, spec):
            # pre-padding from reduction factor(teacher forcing) to feed decoder input in training
            # matching r-times
            t = spec.size(0)
            n_pads = self.Hparams.reduction - (t % self.Hparams.reduction)\
                if t % self.Hparams.reduction != 0 else 0
            pad = (0, 0, 0, n_pads)
            padded_spec = torch.nn.functional.pad(spec, pad, mode='constant', value=0)
            return padded_spec
    hparams = Hparams()
    ap = AudioPreprocessing(hparams)
    
    for idx, (path, _) in enumerate(alignments.items()):
        audio, _ = torchaudio.load(path)
        
        # 기존
        spec = ap.spectrogram(audio)
        mel = ap.melspectrogram(audio)
        
        # 패딩 후
        padded_spec = ap.pre_padding(spec)
        padded_mel = ap.pre_padding(mel)
        print('Original Spec: {} \nPadded Spec: {}\
                \nOriginal Mel: {} \nPadded Mel:{} \n'.format(
            spec.size(), padded_spec.size(),
            mel.size(), padded_mel.size()))
        if idx==4:
            break
            
            
    ####
    Original Spec: torch.Size([608, 513]) 
    Padded Spec: torch.Size([610, 513])            
    Original Mel: torch.Size([608, 80]) 
    Padded Mel:torch.Size([610, 80]) 
    
    Original Spec: torch.Size([685, 513]) 
    Padded Spec: torch.Size([685, 513])            
    Original Mel: torch.Size([685, 80]) 
    Padded Mel:torch.Size([685, 80]) 
    
    Original Spec: torch.Size([304, 513]) 
    Padded Spec: torch.Size([305, 513])            
    Original Mel: torch.Size([304, 80]) 
    Padded Mel:torch.Size([305, 80]) 
    
    Original Spec: torch.Size([398, 513]) 
    Padded Spec: torch.Size([400, 513])            
    Original Mel: torch.Size([398, 80]) 
    Padded Mel:torch.Size([400, 80]) 
    
    Original Spec: torch.Size([230, 513]) 
    Padded Spec: torch.Size([230, 513])            
    Original Mel: torch.Size([230, 80]) 
    Padded Mel:torch.Size([230, 80])

     

    아 그리고 제가 아직 torchaudio를 잘 못다뤄서 그런지

    Spectrogram을 Audio로 변환할 때 많은 어려움을 겪어서

    Numpy, Librosa를 이용한 클래스도 만들었습니다.

    torchaudio가 좀 어렵다 싶으면 아래 코드블럭으로 작성된

    클래스를 가지고 진행해야할 듯 싶네요.

     

    기본적으로 Keithito 님이 만든 오픈소스 Tacotron에서

    거의 대부분을 가져왔습니다.

    사용 방법은 거의 비슷하지만 audio를 읽어오는 과정에서

    조금 차이가 있기 때문에

    코드 블럭 아래에 별도로 남겨두겠습니다.

     

    import torch, librosa
    import numpy as np
    
    
    class AudioPreprocessing():
        def __init__(self, Hparams):
            self.Hparams = Hparams
    
        def spectrogram(self, audio):
            D = self._stft(audio)
            S = self._amp_to_db(np.abs(D)) - self.Hparams.ref_level_db
            return torch.Tensor(S.T)
    
        def melspectrogram(self, audio):
            D = self._stft(audio)
            S = self._amp_to_db(self._linear_to_mel(np.abs(D))) - self.Hparams.ref_level_db
            return torch.Tensor(S.T)
    
        def _stft(self, audio):
            return librosa.stft(audio, 
                                 n_fft=self.Hparams.n_fft, 
                                 hop_length=self.Hparams.hop_length, 
                                 win_length=self.Hparams.win_length, 
                                 pad_mode='constant')
    
        def _istft(self, spec):
            return librosa.istft(spec, 
                                 hop_length=self.Hparams.hop_length, 
                                 win_length=self.Hparams.win_length)
    
        def _linear_to_mel(self, spec):
            _mel_basis = self._build_mel_basis()
            return np.dot(_mel_basis, spec)
    
        def _build_mel_basis(self):
            if self.Hparams.fmax != self.Hparams.sample_rate // 2:
                self.Hparams.fmax = self.Hparams.sample_rate // 2
            return librosa.filters.mel(sr=self.Hparams.sample_rate, 
                                        n_fft=self.Hparams.n_fft, 
                                        n_mels=self.Hparams.n_mels,
                                        fmin=self.Hparams.fmin, 
                                        fmax=self.Hparams.fmax)
    
        def _amp_to_db(self, x):
            min_level = np.exp(self.Hparams.min_level_db / 20 * np.log(10))
            return 20 * np.log10(np.maximum(min_level, x))
    
        def _db_to_amp(self, x):
            return np.power(10.0, (x) * 0.05)
        
        def pre_padding(self, spec):
            # pre-padding from reduction factor(teacher forcing) to feed decoder input in training
            # matching r-times
            if type(spec) != torch.Tensor:
                spec = torch.Tensor(spec)
            t = spec.size(0)
            n_pads = self.Hparams.reduction - (t % self.Hparams.reduction)\
                if t % self.Hparams.reduction != 0 else 0
            pad = (0, 0, 0, n_pads)
            padded_spec = torch.nn.functional.pad(spec, pad, mode='constant', value=0)
            return padded_spec
    
        def spec_to_audio(self, spec):
            if type(spec) == torch.Tensor:
                spec = spec.detach().cpu().numpy()
            S = self._db_to_amp(spec + self.Hparams.ref_level_db)
            audio = self._griffin_lim(S**1.1)
            return audio
    
        def _griffin_lim(self, S):
            if type(S) == torch.Tensor:
                S = S.detach().cpu().numpy()
            angles = np.exp(2j * np.pi * np.random.rand(*S.shape))
            S_complex = np.abs(S).astype(np.complex)
            y = self._istft(S_complex * angles)
            for i in range(self.Hparams.griffin_lim_iters):
                angles = np.exp(1j * np.angle(self._stft(y)))
                y = self._istft(S_complex * angles)
            return y
    hparams = Hparams()
    ap = AudioPreprocessing(hparams)
    
    with open(os.path.join(data_path, 'alignments.json'), 'r', encoding='utf-8') as f:
        alignments = json.loads(f.read())
        for idx, (path, _) in enumerate(alignments.items()):
            audio, _ = librosa.load(path, hparams.sample_rate)
    
            # 기존
            spec = ap.spectrogram(audio)
            mel = ap.melspectrogram(audio)
    
            # 패딩 후
            padded_spec = ap.pre_padding(spec)
            padded_mel = ap.pre_padding(mel)
            print('Original Spec: {} \nPadded Spec: {}\
                    \nOriginal Mel: {} \nPadded Mel:{} \n'.format(
                spec.size(), padded_spec.size(),
                mel.size(), padded_mel.size()))
            if idx==4:
                break
                
                
    ####
    Original Spec: torch.Size([304, 513]) 
    Padded Spec: torch.Size([305, 513])                
    Original Mel: torch.Size([304, 80]) 
    Padded Mel:torch.Size([305, 80]) 
    
    Original Spec: torch.Size([343, 513]) 
    Padded Spec: torch.Size([345, 513])                
    Original Mel: torch.Size([343, 80]) 
    Padded Mel:torch.Size([345, 80]) 
    
    Original Spec: torch.Size([152, 513]) 
    Padded Spec: torch.Size([155, 513])                
    Original Mel: torch.Size([152, 80]) 
    Padded Mel:torch.Size([155, 80]) 
    
    Original Spec: torch.Size([199, 513]) 
    Padded Spec: torch.Size([200, 513])                
    Original Mel: torch.Size([199, 80]) 
    Padded Mel:torch.Size([200, 80]) 
    
    Original Spec: torch.Size([146, 513]) 
    Padded Spec: torch.Size([150, 513])                
    Original Mel: torch.Size([146, 80]) 
    Padded Mel:torch.Size([150, 80])

     

     


    Text Pre-processing

     

    텍스트 데이터 전처리에 사용한 함수를 클래스화하여 만들었습니다.

     

    text_to_sequence() 함수에서 달라진 부분은 두 번째 if 문이 있는 걸 볼 수 있습니다.

    지정하지 않은 이외의 문자를 생각하기엔 골칫거리 같아서

    그냥 처리 하지 않는 것으로 결정했습니다.

    (혹시 좋은 방법 알고 계신 분은 댓글 부탁드립니다.)

     

    또 달라진 점은 sequence_to_text() 부분인데요, 

    기존에 있던 sequence_to_text() 함수에서 tokens를 지정한 후에

    원래의 sequence에서 tokens에 입력된 값을 제거하는 것입니다.

     

    나중에 출력해서 볼 때 보기 편하려고 한 것일 뿐이니, 원하지 않으시면 해당 부분을 지워도 됩니다.

    from jamo import hangul_to_jamo
    
    
    class TextPreprocessing():
        def __init__(self, Hparams):
            self.Hparams = Hparams
        
        # text -> sequence
        def text_to_sequence(self, text):
            sequence = []
            if not 0x1100 <= ord(text[0]) <= 0x1113:
                text = ''.join(list(hangul_to_jamo(text)))
            for t in text:
                if t in self.Hparams._symbol_to_id:
                    # 지정하지 않은 이외의 문자는 시퀀스에 포함하지 않음
                    sequence.append(self.Hparams._symbol_to_id[t]) 
    
            sequence.append(self.Hparams._symbol_to_id['~'])
            return sequence
        
        # sequence -> text without PAD, EOS tokens
        def sequence_to_text(self, sequence):
            tokens = [self.Hparams._symbol_to_id[self.Hparams.PAD],
                      self.Hparams._symbol_to_id[self.Hparams.EOS]]
            sequence = [s for s in sequence if s not in tokens]
            
            text = ''
            for symbol_id in sequence:
                if symbol_id in self.Hparams._id_to_symbol:
                    t = self.Hparams._id_to_symbol[symbol_id]
                    text += t
            return text

     

    사용 방법은 아래를 참고해주세요.

    hparams = Hparams()
    tp = TextPreprocessing(hparams)
    
    origin_text = '안녕하세요?'
    sequence = tp.text_to_sequence(origin_text)
    restructured_text = tp.sequence_to_text(sequence)
    
    print("Sequence: {} \nText: {}\n".format(sequence, restructured_text))
    
    
    Sequence: [13, 21, 45, 4, 27, 62, 20, 21, 11, 26, 13, 33, 73, 1] 
    Text: 안녕하세요?

     

     

     


    데이터셋 구성

     

    Tacotron 1 논문 5페이지에 있는 MODEL DETAILS 부분입니다. 드래그 된 부분을 보시면, L1 loss를 사용했으며 decoder로부터 생성된 mel과 그 mel이 decoder 뒷 단에 있는 post-processing net(=post CBHG, CBHG는 Convolusion Bank Highway Network의 약자입니다.)을 지나서 생성된 linear-scale spectrogram(spec)을 각각 input으로 하여금 target과 비교하여 loss를 구하게 된다고 적혀있습니다. 따라서 데이터셋은 input 값으로 text가 필수적이며(필요 시 decoder 입력값을 지정해도 됨-teacher forcing) 생성된 mel과 spec의 target이 되는 mel과 spec을 별도로 만들어줘야 합니다. AudioPreprocessing 클래스에서 mel과 spec을 만드는 부분은 이 때문입니다. 그리고 바로 밑 줄을 보시면, batch size는 32를 사용했고, 각 배치마다 max length 만큼 padding을 했으며 loss mask를 사용했다고 합니다.

     

     

    Loss와 Teacher Forcing 관련해서는 아래의 링크들을 참고했습니다.

     

     

    L1 Loss 관련 링크

    https://pytorch.org/docs/stable/generated/torch.nn.L1Loss.html

     

    L1Loss — PyTorch 1.9.0 documentation

    Shortcuts

    pytorch.org

     

    Teacher Forcing 관련 링크

    https://kh-kim.gitbook.io/natural-language-processing-with-pytorch/00-cover-9/05-teacher-forcing

     

    자기회귀 속성과 Teacher Forcing 훈련 방법

     

    kh-kim.gitbook.io

     

     

    데이터셋 구성으로 필요한 요소들이 정해졌습니다.

    입력값으로 사용될 Text

    Encoder와 Decoder를 지나서 생성된 Mel의 Loss를 산정할 True Mel

    그리고 생성된 Mel이 Post processing net을 지나서 생성된

    Linear-Scale Spectrogram의 Loss를 산정할 True Spec

    이렇게 총 세 가지의 데이터가 필요합니다.

     

    이전 포스팅에서 저 세 가지의 전처리 방법에 대해서 배웠으므로

    전처리함과 동시에 원하는 파일형태(npy, pt)로 저장하고

    Custom Dataset을 만들어서 DataLoader에 입력해주면 됩니다.

     

     

     


    전처리 (Pre-processing)

     

    전처리 함수는 다음과 같습니다.

    hparams, ap, tp, data_length를 인자로 받으며, 

    data_length를 지정하지 않을 경우 데이터 전체를 전처리합니다. 

    데이터 양이 많아서 시간이 오래걸리므로,

    실험적으로 진행할 때를 위해 별도로 추가했습니다.

    입력하지 않으셔도 무방합니다.

     

    폴더를 생성할 때 기존 경로에 같은 이름의 폴더가 존재한다면

    강제로 삭제하고 다시 생성합니다.

     

    저장하기 직전 pre_padding을 진행하여

    Reduction Factor의 배수에 맞게 패딩합니다.

     

    저는 npy 형식 대신 pt 를 사용해 저장했습니다.

     

    여기서 중요한 부분이 있는데, 아래의 전처리 코드에서

    audio를 linear, mel spectrogram으로 변환할 때 각 spectrogram에 대해

    normalizing을 진행해야합니다.

     

    머신러닝을 조금이라도 공부해봤다면 바로 이해할 수 있을텐데요,

    image, time series를 이용한 task에서 각 feature에 대해 

    scaling을 해주어 값을 소수점 이하로 낮추는 걸 보셨을겁니다.

     

    spectrogram에 normalizing을 해주는 작업 또한 위와 차이가 없는데

    이는 학습이 진행되는 동안 데이터간 값의 편차가 클 경우 학습이 잘 안되기 때문에

    값을 낮춤과 동시에 어느정도 차이를 부여하여 학습이 잘 되게끔 한다고 이해하면 될 것 같습니다.

     

    audio 또한 마찬가지로 녹음된 데이터는 음원의 크기가 제각각이기 때문에

    정상적인 학습을 위해서는 audio에 대해서도 normalizing을 진행해주어야 합니다.

    def _audio_normalize(audio):
    	return audio / np.abs(audio).max()
    
    def _normalize(S, hparams):
        return hparams.max_abs_value * ((S - hparams.min_level_db) / (-hparams.min_level_db))
    
    def _denormalize(D, hparams):
        return ((D * -hparams.min_level_db / hparams.max_abs_value) + hparams.min_level_db)

    위의 코드를 AudioPreprocessing 모듈에 추가하든지 그냥 함수로 두고 사용하든지 편한대로 하면 됩니다.

    spectrogram은 log scale(또는 db scale)로 변환된 상태, 즉 값이 0~-80(또는 20~-100) 사이에서

    normalizing이 진행됨에 따라 0~1(또는 0~max_abs_value)로 변환됩니다.

    따라서 학습 중간중간 또는 추론 할 때에는

    값을 원래의 scale로 변환한 다음 audio로 변환해주어야 합니다.

    그 때 사용되는 함수가 _denormalize() 라고 보면 됩니다.

     

    import os, json, shutil, librosa
    
    
    
    hparams = Hparams()
    ap = AudioPreprocessing(hparams)
    tp = TextPreprocessing(hparams)
    
    
    def preprocessing(hparams, ap, tp, data_length=None):
        alignments_path = os.path.join(hparams.data_dir, 'alignments.json')
        
        with open(alignments_path, 'r', encoding='utf-8') as f:
            alignments = json.loads(f.read())
            
            # 디렉토리 생성
            data_dirs = [hparams.out_texts_dir, 
                         hparams.out_specs_dir,
                         hparams.out_mels_dir]
            for data_dir in data_dirs:
                if os.path.isdir(data_dir): # 이미 있으면 강제 삭제
                    shutil.rmtree(data_dir)
                os.makedirs(data_dir, exist_ok=True) # 디렉토리 생성
            
            # pre-processing
            for idx, (audio_path, text) in enumerate(alignments.items()):
                # text pre-processing
                seq = tp.text_to_sequence(text)
                
                # audio load
                audio, _ = librosa.load(audio_path, sr=hparams.sample_rate, mono=True)
                
                # audio normalizing
                audio = _audio_normalize(audio)
                
                # audio pre-processing
                spec = _normalize(ap.spectrogram(torch.from_numpy(audio)))
                mel = _normalize(ap.melspectrogram(torch.from_numpy(audio)))
                
                # padded
                spec = ap.pre_padding(spec)
                mel = ap.pre_padding(spec)
                
                # text save
                torch.save(seq, os.path.join(hparams.out_texts_dir, 'kss-text-%05d.pt' % idx))
                torch.save(spec, os.path.join(hparams.out_specs_dir, 'kss-spec-%05d.pt' % idx))
                torch.save(mel, os.path.join(hparams.out_mels_dir, 'kss-mel-%05d.pt' % idx))
                
                if data_length != None and idx == data_length:
                    break
            
            
    preprocessing(hparams, ap, tp, data_length=200)

     

     

    Numpy, Librosa를 이용한 클래스로

    Pre-processing을 할 때는 다음과 같습니다.

     

    import os, json, shutil, librosa
    
    
    
    hparams = Hparams()
    ap = AudioPreprocessing(hparams)
    tp = TextPreprocessing(hparams)
    
    
    def preprocessing(hparams, ap, tp, data_length=None):
        alignments_path = os.path.join(hparams.data_dir, 'alignments.json')
        
        with open(alignments_path, 'r', encoding='utf-8') as f:
            alignments = json.loads(f.read())
            
            # 디렉토리 생성
            data_dirs = [hparams.out_texts_dir, 
                         hparams.out_specs_dir,
                         hparams.out_mels_dir]
            for data_dir in data_dirs:
                if os.path.isdir(data_dir): # 이미 있으면 강제 삭제
                    shutil.rmtree(data_dir)
                os.makedirs(data_dir, exist_ok=True) # 디렉토리 생성
            
            # pre-processing
            for idx, (audio_path, text) in enumerate(alignments.items()):
                # text pre-processing
                seq = tp.text_to_sequence(text)
                
                # audio load
                audio, _ = librosa.load(audio_path, hparams.sample_rate)
                
                # audio normalizing
                audio = _audio_normalize(audio)
                
                # audio pre-processing
                spec = _normalize(ap.spectrogram(audio))
                mel = _normalize(ap.melspectrogram(audio))
                
                # padded
                spec = ap.pre_padding(spec)
                mel = ap.pre_padding(mel)
                
                # text save
                torch.save(seq, os.path.join(hparams.out_texts_dir, 'kss-text-%05d.pt' % idx))
                torch.save(spec, os.path.join(hparams.out_specs_dir, 'kss-spec-%05d.pt' % idx))
                torch.save(mel, os.path.join(hparams.out_mels_dir, 'kss-mel-%05d.pt' % idx))
                
                if data_length != None and idx+1 == data_length:
                    break
    
    
    preprocessing(hparams, ap, tp, data_length=200)

     

     


    커스텀 데이터셋 (Custom Dataset)

     

    pytorch custom dataset 관련 링크

    https://wikidocs.net/57165

     

    위키독스

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

    wikidocs.net

     

     

    제가 만든 커스텀 데이터셋은 아래와 같습니다.

    import os, glob, torch
    
    
    class KSSDataset(torch.utils.data.Dataset):
        def __init__(self, Hparams):
            self.Hparams = Hparams
            self.text_paths = glob.glob(os.path.join(self.Hparams.out_texts_dir, '*.pt'))
            self.mel_paths = glob.glob(os.path.join(self.Hparams.out_mels_dir, '*.pt'))
            self.spec_paths = glob.glob(os.path.join(self.Hparams.out_specs_dir, '*.pt'))
        
        def __len__(self):
            return len(self.text_paths)
        
        def __getitem__(self, idx):
            self.texts = torch.LongTensor(torch.load(self.text_paths[idx]))
            self.mels = torch.FloatTensor(torch.load(self.mel_paths[idx]))
            self.specs = torch.FloatTensor(torch.load(self.spec_paths[idx]))
            return self.texts, self.mels, self.specs

    많은 분들께서 알고계실텐데, 원래 보편적으로

    __init__ 부분에 데이터를 선언하거나 불러와서 __getitem__ 부분으로 넘겨줍니다.

     

    하지만 KSS 데이터셋 같은 경우는 대용량 데이터셋입니다.

    모든 데이터를 메모리에 올리는 것은 웬만한 사양이 아니고서는 현실적으로 어렵습니다.

     

    따라서 __init__ 부분에는 각 데이터의 전체 경로만 명시해주고,

    __getitem__ 부분에서 idx 번째의 데이터를 로드하는 형식으로 코드를 작성했습니다.

     

    이렇게 하게되면, Dataset이 DataLoader에 입력되었을 때

    DataLoader의 batch_size 인자 값만큼 데이터가 로드되기 때문에

    메모리 활용 면에서 훨씬 효율적이라고 볼 수 있습니다.

     

    하지만 이는 매 batch 마다 데이터를 로드해서

    학습 속도면에서는 떨어진다고 생각하시면 좋을 것 같습니다.

    그럼에도 불구하고 저는 컴퓨터 사양이 딱히 좋은편이 아니라 위와 같이 진행하도록 하겠습니다.

     

    그리고 중요한 또 한가지는, 데이터를 batch_size 만큼 묶어서 모델에 전달해야 하는데,

    아직 패딩을 하지 않아서 각 데이터의 길이는 모두 천차만별일 겁니다.

     

    그렇기 때문에 batch_size 만큼 묶이지 못하고 에러가 발생하게 됩니다.

     

    이 부분은 전처리를 할 때 패딩을 적용해서 저장하면 간단한 문제이지만,

    방금 언급했다시피 KSS 데이터셋은 아주 용량이 큰 데이터셋이므로

    패딩만큼의 용량도 나중에 데이터를 불러올 때 그만큼 시간이 걸리게 됩니다.

     

    이때 torch.utils.data.DataLoader()의 collate_fn 기능을 이용해서,

    각 batch를 로드할 때마다 torch.nn.utils.rnn.pad_sequence()를 적용해

    간단하게 해결할 수 있습니다.

    패딩뿐만 아니라 다른 작업도 추가할 수 있습니다.

     

     


    앞으로 pytorch를 이용해 데이터셋을 구축하게 된다면,

    collate_fn는 아주 유용한 기능이 될 것이므로

    꼭 아래의 링크를 들어가셔서 참고해주시면 좋겠습니다.

    https://discuss.pytorch.org/t/how-to-create-a-dataloader-with-variable-size-input/8278/3?u=ptrblck 

     

    How to create a dataloader with variable-size input

    By default, torch stacks the input image to from a tensor of size N*C*H*W, so every image in the batch must have the same height and width. In order to load a batch with variable size input image, we have to use our own collate_fn which is used to pack a b

    discuss.pytorch.org

     

     

    제가 구현한 collate_fn은 아래와 같습니다.

    나중에 Decoder에서 mel을 생성할 때 mel_lengths를 기준으로 mel을 생성하게 하려고 하는데,

    제가 작성한 코드대로 mel_lengths를 지정하게 되면 패딩 되기 전의 길이 만큼이 저장됩니다.

     

    아 그리고 mel_lengths는 loss를 구할 때 mask를 생성할 목적으로도 사용되니,

    나중에 유의깊게 봐주세요 !

     

    import torch
    
    
    def collate_fn(batch):
        texts, mels, specs = zip(*batch)
        mel_lengths = torch.LongTensor([mel.size(0) for mel in mels])
        text_pads = torch.nn.utils.rnn.pad_sequence(texts, batch_first=True)
        mel_pads = torch.nn.utils.rnn.pad_sequence(mels, batch_first=True)
        spec_pads = torch.nn.utils.rnn.pad_sequence(specs, batch_first=True)
        return (text_pads.contiguous(), 
                 mel_pads.contiguous(), 
                 spec_pads.contiguous(), 
                 mel_lengths.contiguous())

     

    구현한 collate_fn을 이용하여 data를 출력해보겠습니다.

     

    DataLoader의 사용 방법은 아래의 링크를 참고해주세요.

    https://pytorch.org/docs/stable/data.html

     

    torch.utils.data — PyTorch 1.9.0 documentation

    torch.utils.data At the heart of PyTorch data loading utility is the torch.utils.data.DataLoader class. It represents a Python iterable over a dataset, with support for These options are configured by the constructor arguments of a DataLoader, which has si

    pytorch.org

    dataset = KSSDataset(hparams)
    
    dataloader = torch.utils.data.DataLoader(dataset, 
                                             batch_size=4, 
                                             shuffle=True, 
                                             collate_fn=collate_fn)
    
    for step, values in enumerate(dataloader):
        texts, mels, specs, mel_lengths = values
        print("Texts: {} \nMels: {} \nSpecs: {} \nMel-Lengths: {} \n".format(
            texts.size(), mels.size(), specs.size(), mel_lengths))
        
        if step == 4:
            break
            
            
    
    ####
    # Texts, Padded Mels, Padded SPecs, Mel-Lengths
    Texts: torch.Size([4, 40]) 
    Mels: torch.Size([4, 590, 513]) 
    Specs: torch.Size([4, 590, 513]) 
    Mel-Lengths: tensor([590, 370, 550, 425]) 
    
    Texts: torch.Size([4, 54]) 
    Mels: torch.Size([4, 745, 513]) 
    Specs: torch.Size([4, 745, 513]) 
    Mel-Lengths: tensor([745, 540, 380, 495]) 
    
    Texts: torch.Size([4, 52]) 
    Mels: torch.Size([4, 760, 513]) 
    Specs: torch.Size([4, 760, 513]) 
    Mel-Lengths: tensor([640, 760, 390, 430]) 
    
    Texts: torch.Size([4, 58]) 
    Mels: torch.Size([4, 805, 513]) 
    Specs: torch.Size([4, 805, 513]) 
    Mel-Lengths: tensor([670, 430, 360, 805]) 
    
    Texts: torch.Size([4, 58]) 
    Mels: torch.Size([4, 860, 513]) 
    Specs: torch.Size([4, 860, 513]) 
    Mel-Lengths: tensor([660, 860, 510, 495])

    의도한 대로 text, mel, spec을 전처리하면서 저장하고,

    Dataset을 통해 저장된 text, mel, spec을 batch_size만큼 묶어 불러오는 데 성공했습니다.

     

    다음 포스팅에서는 지금까지 작성한 코드를 정리하고

    Tacotron의 Encoder에 해당하는 부분을 구현해보겠습니다.

     

     

     

    전공이 물리학인지라

    하나하나 뜯어보며 결괏값이 어떻게 나오는지 보기 위해 많은 삽질을 하고 있습니다.

     

    혼자서 많은 시도를 해보고 깨우친 점도 많았지만,

    그때마다 기록을 해두지 않았기 때문에

    다시 공부할 때마다 느낀 점은,

     

    "필기가 하나도 안된 깨끗한 전공서적을 다시 보는 느낌"

     

    이라고 말씀드리고 싶습니다 :(

    더 분발해야겠습니다.

     

     

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

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

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

    댓글

Designed by Tistory.