이미지 로딩 중...
AI Generated
2025. 11. 19. · 2 Views
음성 합성 학습 데이터 포맷 완벽 가이드
AI 음성 합성 모델을 학습시킬 때 가장 중요한 것은 데이터 포맷 설계입니다. Text와 Audio Tokens를 결합하는 방법, Timeframe 구조, 그리고 효율적인 DataCollator 설정까지 실무에서 바로 적용할 수 있는 학습 데이터 구성 방법을 상세히 알려드립니다.
목차
- Text + Audio Tokens 포맷 설계
- Timeframe 구조 이해
- Causal Language Modeling 목표 설정
- DataCollator 설정
- Max Sequence Length 결정
- Train Validation Split 전략
1. Text + Audio Tokens 포맷 설계
시작하며
여러분이 음성 합성 모델을 처음 학습시킬 때 이런 고민을 하셨나요? "텍스트와 오디오를 어떻게 하나의 시퀀스로 만들지?" 많은 개발자들이 이 단계에서 막힙니다.
실제로 음성 합성은 단순히 텍스트를 입력하고 음성을 출력하는 것이 아닙니다. 모델이 이해할 수 있는 형태로 텍스트와 오디오를 통합된 시퀀스로 변환해야 하죠.
이 과정에서 포맷을 잘못 설계하면 모델이 제대로 학습되지 않거나, 추론 시 이상한 결과가 나올 수 있습니다. 바로 이럴 때 필요한 것이 Text + Audio Tokens 결합 포맷입니다.
이 방식을 사용하면 Language Model이 텍스트를 보고 적절한 오디오 토큰을 생성하도록 학습시킬 수 있습니다.
개요
간단히 말해서, 이 개념은 텍스트 토큰과 오디오 토큰을 하나의 연속된 시퀀스로 결합하는 데이터 포맷 설계 방법입니다. 왜 이 방법이 필요할까요?
최신 음성 합성 모델들은 GPT처럼 동작합니다. 텍스트를 읽고 다음에 올 오디오 토큰을 예측하는 방식이죠.
예를 들어, "안녕하세요"라는 텍스트를 입력하면 이에 해당하는 음성의 오디오 토큰들을 순차적으로 생성합니다. 이를 위해서는 학습 데이터가 [텍스트 토큰들...
- 오디오 토큰들...] 형태로 구성되어야 합니다. 기존에는 텍스트와 오디오를 분리해서 Encoder-Decoder 구조로 학습했다면, 이제는 하나의 통합된 시퀀스로 만들어 Causal Language Modeling 방식으로 학습할 수 있습니다.
이 포맷의 핵심 특징은 세 가지입니다: (1) 텍스트가 먼저, 오디오가 나중에 오는 순서, (2) 특수 토큰으로 텍스트와 오디오 영역을 구분, (3) 오디오는 여러 개의 토큰으로 양자화되어 표현됩니다. 이러한 특징들이 모델이 "텍스트를 읽고 음성을 생성한다"는 인과관계를 학습하는 데 핵심적입니다.
코드 예제
# 텍스트 토큰화
text = "안녕하세요"
text_tokens = tokenizer.encode(text) # [101, 2345, 6789, 102]
# 오디오를 코덱으로 양자화 (예: EnCodec)
audio_waveform = load_audio("hello.wav")
audio_tokens = codec_model.encode(audio_waveform) # [523, 124, 678, 234, ...]
# Text + Audio 결합 시퀀스 생성
sequence = {
'input_ids': text_tokens + [SEP_TOKEN] + audio_tokens,
'labels': [-100] * len(text_tokens) + [-100] + audio_tokens # 텍스트 부분은 loss 계산 안 함
}
# 결과: [텍스트 토큰들... | SEP | 오디오 토큰들...]
# 모델은 텍스트를 보고 오디오 토큰을 생성하도록 학습
설명
이것이 하는 일: 텍스트와 오디오를 하나의 학습 가능한 시퀀스로 변환하여 Language Model이 "텍스트 → 음성" 변환을 학습할 수 있게 만듭니다. 첫 번째로, 텍스트를 일반적인 토크나이저로 토큰화합니다.
예를 들어 BERT 토크나이저나 SentencePiece를 사용해서 "안녕하세요"를 [101, 2345, 6789, 102] 같은 숫자 시퀀스로 변환하죠. 이 부분은 여러분이 이미 잘 아는 일반적인 NLP 전처리와 동일합니다.
그 다음으로, 오디오를 토큰으로 변환하는 것이 핵심입니다. 오디오 파형을 그대로 사용할 수는 없으니, EnCodec이나 SoundStream 같은 Neural Codec을 사용해서 오디오를 이산적인(discrete) 토큰들로 양자화합니다.
이 과정을 통해 연속적인 오디오 신호가 [523, 124, 678, ...] 같은 토큰 시퀀스로 변환됩니다. 마치 텍스트처럼요!
마지막으로, 이 두 시퀀스를 결합합니다. 중요한 점은 labels 설정인데, 텍스트 부분은 -100으로 설정하여 loss 계산에서 제외시킵니다.
왜냐하면 우리는 모델이 "텍스트를 보고 오디오를 생성"하도록 학습시키는 것이지, 텍스트를 생성하도록 학습시키는 게 아니기 때문입니다. 오디오 토큰 부분만 실제 토큰 값으로 설정하여 이 부분에 대해서만 예측 오류를 계산합니다.
여러분이 이 포맷을 사용하면 복잡한 Encoder-Decoder 구조 없이도 간단한 Transformer Decoder만으로 음성 합성이 가능해집니다. 또한 텍스트 길이와 오디오 길이의 관계를 모델이 자동으로 학습하게 되며, Zero-shot 화자 복제 같은 고급 기능도 같은 프레임워크 안에서 구현할 수 있습니다.
실전 팁
💡 텍스트와 오디오 사이에 특수 구분 토큰을 넣어야 모델이 두 영역을 명확히 구분할 수 있습니다. [SEP], [AUDIO_START] 같은 토큰을 추가하세요.
💡 오디오 토큰의 vocabulary size는 보통 1024~2048 정도로 설정합니다. 너무 작으면 음질이 떨어지고, 너무 크면 학습이 어려워집니다.
💡 labels에서 -100을 사용하는 이유는 PyTorch CrossEntropyLoss가 -100 인덱스를 자동으로 무시하기 때문입니다. 다른 프레임워크를 쓴다면 masking 방식이 다를 수 있으니 확인하세요.
💡 실무에서는 텍스트:오디오 비율이 보통 1:101:20 정도입니다. 즉 텍스트 토큰 하나당 오디오 토큰 1020개 정도가 생성됩니다. 이 비율을 미리 파악해두면 sequence length 설정에 도움이 됩니다.
💡 디버깅할 때는 먼저 짧은 단어("안녕")로 시작해서 포맷이 제대로 되는지 확인하세요. 긴 문장으로 바로 시작하면 어디서 문제가 생겼는지 찾기 어렵습니다.
2. Timeframe 구조 이해
시작하며
여러분이 오디오 토큰을 처음 보셨을 때 이런 의문이 들지 않으셨나요? "왜 오디오 토큰이 7개씩 묶여있지?" 단순히 일렬로 나열된 게 아니라 특정 단위로 그룹화되어 있는 것을 보고 당황하실 수 있습니다.
이것은 Neural Codec의 핵심 개념인 Timeframe 구조 때문입니다. 오디오는 시간에 따라 변하는 신호이고, 이를 효율적으로 표현하기 위해 시간 단위(frame)로 나누어 여러 개의 코드북(codebook)으로 양자화합니다.
이 구조를 이해하지 못하면 데이터 로더를 제대로 만들 수 없습니다. 바로 이럴 때 필요한 것이 Timeframe 구조에 대한 정확한 이해입니다.
이를 알면 오디오 데이터를 올바르게 재구성하고, 모델 출력을 다시 음성으로 변환할 수 있습니다.
개요
간단히 말해서, Timeframe 구조는 오디오의 각 시간 프레임을 여러 개의 코드북 인덱스로 표현하는 방식입니다. 보통 1 frame = 7 tokens 형태로 구성됩니다.
왜 이렇게 복잡하게 만들었을까요? 오디오는 정보량이 매우 많습니다.
1초에 16,000개 이상의 샘플 포인트가 있죠. 이를 하나의 토큰으로 압축하면 너무 많은 정보가 손실됩니다.
그래서 EnCodec 같은 최신 코덱은 각 시간 프레임을 여러 레벨(layer)로 나누어 양자화합니다. 예를 들어, 첫 번째 코드북은 전체적인 음색을, 두 번째는 세부적인 피치를, 세 번째는 더 미세한 특징을 담는 식입니다.
이렇게 계층적으로 표현하면 적은 비트로도 고품질 오디오를 재현할 수 있습니다. 기존의 단순한 VQ-VAE는 1 frame = 1 token이었다면, 이제는 1 frame = multiple tokens (보통 7개)로 표현하여 훨씬 풍부한 정보를 담을 수 있습니다.
핵심 특징은: (1) 각 frame은 동일한 시간 위치의 여러 양자화 레벨을 나타냄, (2) 7개 토큰이 계층적으로 정보를 분담, (3) 디코딩 시 이 7개를 모두 사용해야 원본 음성 품질이 나옴. 이 구조를 제대로 다루지 않으면 생성된 음성이 찌그러지거나 이상한 노이즈가 발생합니다.
코드 예제
# EnCodec 출력: [batch, num_codebooks, num_frames]
# 예: [1, 7, 200] -> 7개 코드북, 200개 프레임
audio_codes = codec_model.encode(waveform) # shape: [1, 7, 200]
# Timeframe 구조로 재배열: [num_frames, num_codebooks]
frames = audio_codes.squeeze(0).transpose(0, 1) # [200, 7]
# 각 프레임을 하나의 토큰 그룹으로 flatten
# frame 0: [c0_t0, c1_t0, c2_t0, c3_t0, c4_t0, c5_t0, c6_t0]
# frame 1: [c0_t1, c1_t1, c2_t1, c3_t1, c4_t1, c5_t1, c6_t1]
audio_tokens = frames.reshape(-1) # [1400] = 200 frames * 7 tokens
# 디코딩 시 다시 복원
frames_restored = audio_tokens.reshape(200, 7)
audio_codes_restored = frames_restored.transpose(0, 1).unsqueeze(0)
waveform_decoded = codec_model.decode(audio_codes_restored)
설명
이것이 하는 일: 오디오의 각 시간 단위(frame)를 7개의 계층적 토큰으로 표현하여, 적은 용량으로도 고품질 음성을 재현할 수 있게 만듭니다. 첫 번째로, EnCodec 같은 Neural Codec은 오디오를 인코딩할 때 [batch, num_codebooks, num_frames] 형태로 출력합니다.
예를 들어 1초짜리 오디오를 인코딩하면 [1, 7, 75] 같은 텐서가 나옵니다. 여기서 7은 코드북 개수(양자화 레벨), 75는 시간 프레임 개수입니다.
EnCodec는 보통 초당 75 프레임으로 인코딩하므로, 1초 = 75 프레임이 됩니다. 그 다음으로, 이 구조를 Language Model이 다룰 수 있는 1D 시퀀스로 변환해야 합니다.
중요한 점은 "같은 시간의 7개 토큰을 묶어서" 처리해야 한다는 것입니다. transpose를 사용해서 [num_frames, num_codebooks] 형태로 바꾼 후, flatten하면 자연스럽게 frame 단위로 묶인 시퀀스가 됩니다.
즉 [프레임0의 7개 토큰, 프레임1의 7개 토큰, ...] 순서가 되는 거죠. 마지막으로, 디코딩할 때는 이 과정을 역으로 수행합니다.
1D 시퀀스를 다시 [num_frames, 7]로 reshape하고, transpose로 [7, num_frames]로 만든 후 EnCodec 디코더에 넣으면 원본 오디오 파형이 복원됩니다. 이 순서를 하나라도 틀리면 엉뚱한 소리가 나오거나 오류가 발생합니다.
여러분이 이 구조를 정확히 이해하면, 모델 출력을 올바르게 오디오로 변환할 수 있고, 데이터 증강이나 프레임 단위 처리도 가능해집니다. 또한 특정 프레임만 수정하거나, 프레임 단위로 음성을 편집하는 고급 기능도 구현할 수 있습니다.
실전 팁
💡 EnCodec의 기본 설정은 7개 코드북이지만, bandwidth를 조절하면 4개나 8개로도 바꿀 수 있습니다. 프로젝트 초기에 이 값을 고정하고 일관되게 사용하세요.
💡 프레임 레이트는 보통 75Hz (초당 75프레임)입니다. 즉 1초 오디오 = 75 프레임 = 525 토큰(75×7)입니다. 이를 기준으로 max sequence length를 계산하세요.
💡 디버깅할 때 가장 흔한 실수는 transpose 순서를 틀리는 것입니다. 항상 shape을 print해서 [num_frames, 7] → flatten → [num_tokens] 순서가 맞는지 확인하세요.
💡 실무에서 음질이 이상하다면 코드북 순서가 섞였을 가능성이 큽니다. 첫 번째 코드북(인덱스 0)이 가장 중요한 정보를 담으므로, 이것만 사용해서 디코딩해보면 대략적인 음색을 확인할 수 있습니다.
💡 메모리 절약을 위해 모든 7개 코드북을 다 쓰지 않고 처음 4개만 사용하는 방법도 있습니다. 품질은 조금 떨어지지만 토큰 수가 절반 이하로 줄어듭니다.
3. Causal Language Modeling 목표 설정
시작하며
여러분이 음성 합성 모델을 학습시킬 때 이런 질문을 하셨나요? "Seq2Seq를 써야 하나, 아니면 GPT처럼 학습시켜야 하나?" 최신 TTS 모델들은 점점 더 GPT 스타일의 Causal Language Modeling 방식을 선호합니다.
그 이유는 간단합니다. Causal LM은 구조가 단순하면서도 강력하고, 무엇보다 대규모 사전학습 언어 모델의 지식을 활용할 수 있기 때문입니다.
하지만 음성 합성에 Causal LM을 적용하려면 몇 가지 중요한 설정이 필요합니다. 특히 "어느 부분을 예측하고, 어느 부분은 입력으로만 사용할지"를 명확히 해야 합니다.
바로 이럴 때 필요한 것이 올바른 Causal Language Modeling 목표 설정입니다. 이를 제대로 하면 모델이 텍스트를 이해하고 자연스러운 음성을 생성하도록 학습됩니다.
개요
간단히 말해서, Causal Language Modeling은 "이전 토큰들을 보고 다음 토큰을 예측하는" 학습 방식입니다. 음성 합성에서는 텍스트 토큰을 보고 오디오 토큰을 예측하도록 설정합니다.
왜 이 방식이 음성 합성에 효과적일까요? 전통적인 Seq2Seq는 Encoder와 Decoder를 따로 학습시켜야 하고, Attention 메커니즘도 복잡합니다.
반면 Causal LM은 단일 Transformer Decoder만 있으면 되고, GPT-2나 LLaMA 같은 사전학습 모델을 fine-tuning할 수도 있습니다. 예를 들어, LLM이 이미 학습한 언어 이해 능력을 그대로 활용하면서 오디오 생성 능력만 추가로 학습시킬 수 있죠.
기존에는 "텍스트 전체를 Encode → 오디오 전체를 Decode"하는 방식이었다면, 이제는 "[텍스트 토큰들... + 오디오 토큰들...]을 왼쪽부터 순차적으로 예측"하는 방식입니다.
핵심 특징은: (1) Auto-regressive 생성 (한 번에 한 토큰씩), (2) 텍스트 부분은 입력으로만 사용하고 loss는 오디오 부분에만 적용, (3) Attention Mask로 미래 토큰을 못 보게 함. 이 설정이 모델이 "텍스트를 읽고 → 음성을 생성"하는 인과관계를 학습하게 만듭니다.
코드 예제
from transformers import GPT2LMHeadModel, DataCollatorForLanguageModeling
# 시퀀스 구성: [text_tokens + SEP + audio_tokens]
input_ids = [101, 234, 567, 890, SEP, 1001, 1002, 1003, ...] # 텍스트 4개 + 오디오 N개
# Labels 설정: 텍스트 부분은 -100 (loss 계산 제외)
labels = [-100, -100, -100, -100, -100] + [1001, 1002, 1003, ...]
# 학습 데이터 생성
training_sample = {
'input_ids': torch.tensor(input_ids),
'labels': torch.tensor(labels),
'attention_mask': torch.ones(len(input_ids)) # Causal mask는 모델 내부에서 자동 적용
}
# Causal LM 학습 - 모델은 오디오 토큰만 예측하도록 학습됨
outputs = model(**training_sample)
loss = outputs.loss # 오디오 토큰 예측 오류만 계산됨
설명
이것이 하는 일: 텍스트를 조건(condition)으로 사용하여 오디오 토큰을 순차적으로 생성하도록 모델을 학습시킵니다. GPT처럼 작동하되, 오디오만 생성합니다.
첫 번째로, input_ids는 텍스트와 오디오가 결합된 전체 시퀀스입니다. 예를 들어 "안녕"이라는 텍스트가 [101, 234, 567, 890]으로 토큰화되고, 그 뒤에 SEP 토큰, 그리고 오디오 토큰들이 이어집니다.
모델은 이 전체 시퀀스를 입력받지만, 학습 목표는 다릅니다. 그 다음으로, labels 설정이 핵심입니다.
텍스트 토큰 위치는 모두 -100으로 설정합니다. PyTorch의 CrossEntropyLoss는 -100 인덱스를 자동으로 무시하므로, 텍스트 부분에 대해서는 loss가 계산되지 않습니다.
오직 오디오 토큰 부분만 실제 토큰 ID를 labels로 설정하여, 이 부분만 예측하도록 합니다. 이렇게 하면 모델은 텍스트를 "읽기만" 하고, 오디오를 "생성"하도록 학습됩니다.
마지막으로, Causal Attention Mask는 GPT2LMHeadModel 같은 모델에서 자동으로 적용됩니다. 즉, 위치 i의 토큰은 위치 0~i까지만 볼 수 있고, i+1 이후는 볼 수 없습니다.
이것이 Auto-regressive 생성의 핵심이며, 추론 시에도 동일한 방식으로 한 토큰씩 생성하게 됩니다. 학습할 때 teacher forcing을 쓰지만, 추론 시에는 자신이 생성한 토큰을 다음 입력으로 사용하는 거죠.
여러분이 이 목표를 올바르게 설정하면 텍스트의 의미를 이해하고 그에 맞는 자연스러운 억양과 발음의 음성을 생성하는 모델을 만들 수 있습니다. 또한 같은 프레임워크로 다화자 학습, 감정 제어, 심지어 대화 생성까지 확장할 수 있습니다.
실전 팁
💡 반드시 labels의 텍스트 부분을 -100으로 설정하세요. 그렇지 않으면 모델이 텍스트도 생성하려고 학습되어, 음성 품질이 크게 떨어집니다.
💡 SEP 토큰 위치도 -100으로 설정하는 게 좋습니다. SEP은 단순한 구분자일 뿐 예측할 필요가 없는 토큰이기 때문입니다.
💡 GPT2Config의 vocab_size를 늘려야 합니다. 일반 텍스트 vocab (50257) + 오디오 vocab (1024~2048) + 특수 토큰들을 모두 포함하도록 설정하세요.
💡 실무에서 학습이 불안정하다면 gradient clipping을 사용하세요. max_grad_norm=1.0 정도로 설정하면 loss가 갑자기 튀는 것을 방지할 수 있습니다.
💡 추론 시에는 temperature와 top_k/top_p sampling을 조절해서 음성의 다양성과 안정성을 조절할 수 있습니다. temperature=0.8, top_p=0.9 정도가 좋은 시작점입니다.
4. DataCollator 설정
시작하며
여러분이 학습 데이터를 DataLoader에 넣을 때 이런 문제를 겪어보셨나요? "시퀀스 길이가 제각각이라 배치를 만들 수 없어!" 음성 데이터는 특히 길이 편차가 심합니다.
1초짜리도 있고 10초짜리도 있죠. 이 문제를 해결하지 않으면 GPU 메모리 낭비가 심하거나, 아예 배치 학습이 불가능합니다.
모든 시퀀스를 최대 길이로 패딩하면 메모리가 터지고, 패딩 없이 배치를 만들려고 하면 텐서 크기가 안 맞아서 오류가 납니다. 바로 이럴 때 필요한 것이 올바른 DataCollator 설정입니다.
Padding과 Truncation을 효율적으로 처리하여 다양한 길이의 음성 데이터를 안정적으로 학습시킬 수 있습니다.
개요
간단히 말해서, DataCollator는 배치 내의 시퀀스들을 동일한 길이로 맞춰주는 유틸리티입니다. Padding token을 추가하고, 너무 긴 시퀀스는 자르며, attention mask를 자동으로 생성합니다.
왜 음성 합성에서 DataCollator가 특히 중요할까요? 일반 NLP는 텍스트만 다루므로 시퀀스 길이 편차가 비교적 작지만, 음성은 길이 편차가 10배 이상 날 수 있습니다.
"안녕"은 50 토큰, "긴 문장..."은 500 토큰 이런 식이죠. 또한 오디오 토큰은 개수가 많아서 메모리 사용량이 큽니다.
예를 들어, 10초 음성 = 약 5,000 토큰이므로, 배치 크기 32면 160,000 토큰을 한 번에 처리해야 합니다. 효율적인 패딩 전략이 없으면 메모리가 부족하거나 학습 속도가 느려집니다.
기존에는 고정 길이로 모든 샘플을 자르거나 패딩했다면, 이제는 배치 내 최대 길이로만 패딩하여 불필요한 메모리 낭비를 줄일 수 있습니다. 핵심 설정은: (1) padding='longest' - 배치 내 최대 길이로만 패딩, (2) max_length - 절대 넘지 않을 최대 길이 지정, (3) pad_to_multiple_of - GPU 효율을 위해 8의 배수로 패딩.
이 세 가지를 조합하면 메모리와 속도를 모두 최적화할 수 있습니다.
코드 예제
from transformers import DataCollatorForLanguageModeling
# DataCollator 설정
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False, # Causal LM이므로 Masked LM은 사용 안 함
pad_to_multiple_of=8, # Tensor Core 최적화를 위해 8의 배수로 패딩
)
# 사용 예시
batch = [
{'input_ids': [101, 234, ..., 890], 'labels': [-100, ..., 890]}, # 길이 100
{'input_ids': [101, 567, ..., 543], 'labels': [-100, ..., 543]}, # 길이 150
]
# Collate 실행 - 배치 내 최대 길이(150)의 8의 배수(152)로 패딩됨
collated_batch = data_collator(batch)
# 결과: input_ids shape = [2, 152], attention_mask shape = [2, 152]
# 길이 100인 샘플은 52개 패딩 토큰 추가됨
설명
이것이 하는 일: 서로 다른 길이의 음성 샘플들을 효율적으로 배치로 묶어서 GPU에서 병렬 처리할 수 있게 만듭니다. 첫 번째로, DataCollatorForLanguageModeling을 생성할 때 mlm=False로 설정합니다.
mlm은 Masked Language Modeling의 약자로, BERT처럼 랜덤하게 토큰을 마스킹하는 방식입니다. 우리는 Causal LM을 쓰므로 이 옵션을 False로 꺼야 합니다.
만약 True로 두면 오디오 토큰이 랜덤하게 마스킹되어 학습이 제대로 안 됩니다. 그 다음으로, pad_to_multiple_of=8 설정이 중요합니다.
GPU의 Tensor Core는 8의 배수 크기의 행렬 연산을 가장 빠르게 처리합니다. 예를 들어 배치 내 최대 길이가 150이면, 152(8의 배수)로 패딩하여 GPU 연산 효율을 높입니다.
단 2개 토큰 차이지만 속도는 10~20% 빨라질 수 있습니다. 마지막으로, DataCollator는 attention_mask도 자동으로 생성합니다.
실제 토큰 위치는 1, 패딩 위치는 0으로 설정하여 모델이 패딩 토큰을 무시하도록 합니다. 또한 labels도 패딩 위치는 -100으로 자동 설정되어 loss 계산에서 제외됩니다.
이 모든 과정이 자동으로 이루어지므로, 여러분은 그냥 DataLoader에 data_collator만 전달하면 됩니다. 여러분이 이 설정을 사용하면 메모리 사용량을 최소화하면서도 빠른 학습 속도를 얻을 수 있습니다.
또한 Dynamic Batching도 구현할 수 있는데, 긴 샘플들끼리, 짧은 샘플들끼리 묶어서 배치를 만들면 패딩 토큰을 더욱 줄일 수 있습니다.
실전 팁
💡 pad_to_multiple_of는 8 또는 16을 사용하세요. A100이나 H100 GPU는 16의 배수에서 더욱 최적화됩니다.
💡 DataCollator는 배치 단위로 동작하므로, 같은 배치 안에서만 길이를 맞춥니다. 배치마다 최대 길이가 다를 수 있으므로 메모리 사용량이 변동될 수 있습니다.
💡 실무에서 OOM(Out of Memory) 오류가 나면, max_length를 DataCollator에 명시적으로 전달하세요. 극단적으로 긴 샘플이 배치에 들어가는 것을 방지할 수 있습니다.
💡 HuggingFace Trainer를 사용할 때는 data_collator 파라미터에 전달하기만 하면 됩니다. Trainer가 자동으로 배치 생성 시 collator를 호출합니다.
💡 커스텀 collator가 필요하다면 DataCollatorForLanguageModeling을 상속받아서 call 메서드만 오버라이드하세요. 예를 들어 화자 ID나 감정 레이블을 추가로 처리할 수 있습니다.
5. Max Sequence Length 결정
시작하며
여러분이 모델 config를 설정할 때 가장 고민되는 부분이 뭔가요? 바로 "max_length를 얼마로 해야 하지?"입니다.
너무 짧으면 긴 문장을 처리 못하고, 너무 길면 메모리가 부족하거나 학습이 느려집니다. 특히 음성 합성은 오디오 토큰이 매우 많아서 이 문제가 심각합니다.
5초짜리 음성도 500~1000 토큰이 필요하니까요. 적절한 max_length를 정하지 않으면 대부분의 샘플이 잘리거나, 반대로 메모리 낭비가 심해집니다.
바로 이럴 때 필요한 것이 데이터 분석 기반의 Max Sequence Length 결정 전략입니다. 실제 데이터 분포를 보고 최적의 길이를 정하면 학습 효율을 크게 높일 수 있습니다.
개요
간단히 말해서, Max Sequence Length는 모델이 처리할 수 있는 최대 토큰 개수입니다. 음성 합성에서는 보통 900~1200 토큰 정도로 설정합니다.
왜 이 값이 중요할까요? Transformer의 Self-Attention은 O(n²) 복잡도를 가집니다.
길이가 2배 늘면 계산량이 4배 늘어나고 메모리도 4배 필요합니다. 예를 들어, max_length=512와 1024의 차이는 단순히 2배가 아니라 메모리와 속도 모두 4배 차이입니다.
따라서 실제 필요한 최소한의 길이로 설정하는 것이 매우 중요합니다. 기존에는 무조건 긴 길이(2048, 4096 등)를 설정했다면, 이제는 데이터 분포를 분석하여 95 percentile 정도를 커버하는 길이를 선택합니다.
핵심 전략은: (1) 데이터셋의 길이 분포 분석, (2) 95~98% 샘플을 커버하는 길이 선택, (3) 텍스트 + 오디오 + 특수 토큰 모두 고려. 이렇게 하면 대부분의 샘플을 처리하면서도 메모리를 절약할 수 있습니다.
코드 예제
import numpy as np
# 데이터셋의 시퀀스 길이 분석
sequence_lengths = []
for sample in dataset:
text_tokens = len(tokenizer.encode(sample['text']))
audio_tokens = len(sample['audio_codes'].reshape(-1)) # flatten된 오디오 토큰 수
total_length = text_tokens + 1 + audio_tokens # +1은 SEP 토큰
sequence_lengths.append(total_length)
# 통계 분석
lengths = np.array(sequence_lengths)
print(f"Mean: {lengths.mean():.0f}")
print(f"Median: {np.median(lengths):.0f}")
print(f"95 percentile: {np.percentile(lengths, 95):.0f}")
print(f"Max: {lengths.max():.0f}")
# 95 percentile 기준으로 max_length 설정
max_length = int(np.percentile(lengths, 95))
# 8의 배수로 올림 (GPU 최적화)
max_length = ((max_length + 7) // 8) * 8
print(f"Recommended max_length: {max_length}")
# 예시 출력: Recommended max_length: 896
설명
이것이 하는 일: 실제 데이터를 분석하여 대부분의 샘플을 처리하면서도 메모리와 속도를 최적화하는 max_length를 찾습니다. 첫 번째로, 전체 데이터셋을 순회하며 각 샘플의 실제 토큰 길이를 계산합니다.
텍스트 토큰 수 + 구분자 1개 + 오디오 토큰 수를 모두 합친 것이 최종 시퀀스 길이입니다. 예를 들어 "안녕하세요" (텍스트 5 토큰) + SEP (1 토큰) + 3초 음성 (약 220 프레임 × 7 = 1540 토큰) = 총 1546 토큰이 되는 식입니다.
이 계산을 모든 샘플에 대해 수행하여 길이 분포를 파악합니다. 그 다음으로, NumPy의 percentile 함수로 통계를 냅니다.
평균과 중앙값도 중요하지만, 95 percentile이 가장 중요합니다. 이 값은 "전체 샘플의 95%가 이 길이 안에 들어온다"는 의미입니다.
예를 들어 95 percentile이 850이면, 850 토큰으로 설정하면 95%의 샘플은 잘리지 않고 처리되고, 5%만 잘립니다. 98 percentile을 쓰면 더 적게 잘리지만 메모리가 더 필요하므로, 95가 좋은 균형점입니다.
마지막으로, 계산된 값을 8의 배수로 올림합니다. 850이 나왔다면 856으로 올림하는 거죠.
이렇게 하면 GPU Tensor Core 최적화를 받을 수 있어 실제 학습 속도가 빨라집니다. 또한 config에 이 값을 max_position_embeddings로도 설정해야 positional encoding이 제대로 작동합니다.
여러분이 이 방법을 사용하면 "너무 길어서 메모리 부족" 또는 "너무 짧아서 샘플 대부분이 잘림" 같은 문제를 피할 수 있습니다. 또한 실험적으로 여러 값을 시도하는 대신, 데이터 기반으로 한 번에 최적값을 찾을 수 있습니다.
실전 팁
💡 데이터셋이 클 때는 전체를 분석하지 말고 랜덤 샘플링으로 1만 개 정도만 분석해도 충분합니다. 분포는 크게 다르지 않습니다.
💡 학습 초기에는 좀 더 짧은 길이(70~80 percentile)로 시작하는 것도 좋은 전략입니다. 빠르게 학습한 후, fine-tuning 단계에서 길이를 늘리면 수렴이 더 안정적입니다.
💡 Multi-GPU 학습 시 배치 크기를 줄여야 할 수 있습니다. max_length=900, batch_size=32가 안 되면 batch_size=16으로 줄이고 gradient_accumulation_steps=2로 보상하세요.
💡 잘리는 5%의 샘플이 중요한 긴 문장들이라면, 별도로 긴 샘플만 모은 데이터셋을 만들어 추가 학습하는 방법도 있습니다.
💡 실무에서는 max_length를 여유있게 설정하는 것보다, 데이터 전처리 단계에서 너무 긴 샘플을 미리 분할하는 것이 더 효율적입니다. 10초 음성은 5초씩 2개로 나누는 식이죠.
6. Train Validation Split 전략
시작하며
여러분이 학습을 시작하기 전 마지막으로 결정해야 할 것이 있습니다. "학습 데이터와 검증 데이터를 어떻게 나누지?" 일반적으로는 80:20이나 90:10으로 나누지만, 음성 합성은 조금 다릅니다.
음성 데이터는 화자별로 특성이 다르고, 녹음 환경도 다릅니다. 단순 랜덤 split을 하면 같은 화자의 같은 문장이 train과 validation에 동시에 들어갈 수 있습니다.
이러면 validation loss는 좋게 나오지만, 실제 새로운 화자나 문장에는 일반화가 안 됩니다. 바로 이럴 때 필요한 것이 올바른 Train/Validation Split 전략입니다.
화자, 문장, 녹음 환경을 고려한 전략적 분할로 진짜 일반화 성능을 측정할 수 있습니다.
개요
간단히 말해서, Train/Validation Split은 학습용 데이터와 평가용 데이터를 나누는 과정입니다. 음성 합성에서는 화자 기준 split이 가장 중요합니다.
왜 화자 기준 split이 필요할까요? 음성 합성의 핵심 능력은 "새로운 화자의 목소리도 잘 합성하는가"입니다.
만약 같은 화자의 데이터가 train과 validation에 모두 들어가면, 모델이 그 화자의 목소리를 외워버려서 validation loss가 부정확하게 좋게 나옵니다. 예를 들어, 화자 A의 100개 문장 중 80개를 학습하고 20개로 검증하면, 모델은 이미 화자 A의 특성을 알고 있으므로 쉽게 맞출 수 있습니다.
하지만 실제 서비스에서는 완전히 새로운 화자 B의 목소리를 합성해야 하죠. 기존에는 전체 샘플을 랜덤하게 80:20으로 나눴다면, 이제는 화자를 먼저 나누고(예: 80명 학습, 20명 검증), 각 화자의 모든 샘플을 해당 set에 넣습니다.
핵심 전략은: (1) Speaker-level split - 화자 단위로 먼저 분할, (2) Stratified split - 성별, 연령 등 균형 유지, (3) 최소 검증 화자 수 확보 (10명 이상). 이렇게 하면 실제 zero-shot 성능을 제대로 평가할 수 있습니다.
코드 예제
import numpy as np
from sklearn.model_selection import train_test_split
# 데이터셋에 화자 정보가 있다고 가정
dataset = [
{'text': '안녕', 'audio': ..., 'speaker_id': 'spk001', 'gender': 'M'},
{'text': '반가워', 'audio': ..., 'speaker_id': 'spk001', 'gender': 'M'},
{'text': '안녕', 'audio': ..., 'speaker_id': 'spk002', 'gender': 'F'},
# ... 수천 개 샘플
]
# 1. 화자별로 그룹화
speaker_to_samples = {}
for sample in dataset:
spk = sample['speaker_id']
if spk not in speaker_to_samples:
speaker_to_samples[spk] = []
speaker_to_samples[spk].append(sample)
# 2. 화자 리스트와 성별 추출 (stratified split을 위해)
speakers = list(speaker_to_samples.keys())
genders = [dataset[0]['gender'] for spk in speakers
for sample in speaker_to_samples[spk][:1]]
# 3. 화자를 train/val로 split (성별 비율 유지)
train_speakers, val_speakers = train_test_split(
speakers, test_size=0.1, stratify=genders, random_state=42
)
# 4. 샘플 분할
train_dataset = [s for spk in train_speakers for s in speaker_to_samples[spk]]
val_dataset = [s for spk in val_speakers for s in speaker_to_samples[spk]]
print(f"Train speakers: {len(train_speakers)}, samples: {len(train_dataset)}")
print(f"Val speakers: {len(val_speakers)}, samples: {len(val_dataset)}")
# 예시: Train speakers: 90, samples: 9500 / Val speakers: 10, samples: 500
설명
이것이 하는 일: 새로운 화자에 대한 진짜 일반화 능력을 평가하기 위해, 화자 기준으로 학습/검증 데이터를 완전히 분리합니다. 첫 번째로, 전체 데이터셋을 화자별로 그룹화합니다.
speaker_to_samples 딕셔너리를 만들어서 각 화자 ID를 key로, 그 화자의 모든 샘플들을 value로 저장합니다. 예를 들어 'spk001'이 100개 샘플을 가지고 있다면, 이 100개가 하나의 그룹이 되는 거죠.
이 단계가 중요한 이유는, 나중에 화자 단위로 split할 때 한 화자의 샘플이 흩어지지 않도록 하기 위함입니다. 그 다음으로, scikit-learn의 train_test_split을 사용하되, stratify 파라미터로 성별 비율을 유지합니다.
만약 전체 화자의 60%가 여성, 40%가 남성이라면, train set과 val set 모두 이 비율을 유지하도록 합니다. 이렇게 하지 않으면 우연히 val set에 남성만 들어가거나 할 수 있고, 그러면 성별 간 성능 차이를 제대로 평가할 수 없습니다.
test_size=0.1은 화자의 10%를 검증용으로 사용한다는 의미입니다. 마지막으로, 선택된 화자들의 모든 샘플을 해당 set에 넣습니다.
train_speakers에 90명이 선택되었고 각 화자가 평균 100개 샘플을 가지고 있다면, train_dataset은 약 9,000개 샘플이 됩니다. 중요한 점은 validation set의 화자들은 모델이 학습 중에 한 번도 본 적 없는 완전히 새로운 화자라는 것입니다.
따라서 validation loss가 진짜 일반화 성능을 반영하게 됩니다. 여러분이 이 전략을 사용하면 과적합을 제대로 감지할 수 있고, 실제 서비스 환경에서의 성능을 정확히 예측할 수 있습니다.
또한 zero-shot voice cloning 같은 기능을 개발할 때, validation set으로 성능을 미리 테스트할 수 있습니다.
실전 팁
💡 최소 10명 이상의 검증 화자를 확보하세요. 3~5명만으로는 통계적으로 의미 있는 평가가 어렵습니다.
💡 가능하면 성별뿐 아니라 연령대, 악센트, 녹음 품질 등도 stratify하세요. 다만 너무 많은 기준을 쓰면 split이 불가능할 수 있으니 2~3개 기준이 적당합니다.
💡 실무에서 데이터가 계속 추가된다면, 초기에 정한 val speakers를 고정하세요. 나중에 추가되는 데이터는 모두 train에만 넣고, val은 변경하지 않아야 성능 추이를 비교할 수 있습니다.
💡 Test set도 별도로 만드는 것을 권장합니다. Train 90% / Val 5% / Test 5% 정도로 3-way split하면, validation으로 하이퍼파라미터를 튜닝하고 test로 최종 성능을 보고할 수 있습니다.
💡 화자 정보가 없는 데이터셋이라면, 오디오 임베딩(예: speaker verification model)으로 자동으로 화자를 클러스터링한 후 split하는 방법도 있습니다. 완벽하진 않지만 랜덤 split보다는 훨씬 낫습니다.