이 글은 한국어 STT를 위한 Whisper fine-tuning 3부작 중 2부다: Preprocessing → Training → Evaluation. 여기서는 전처리된 dataset을 불러와 학습 루프를 돌린다. 1부는 전처리, 3부는 evaluation/benchmarking을 다룰 예정이다.


Mel spectrogram과 tokenized label을 디스크에 저장해 두었다면, 다음은 이걸 training loop에 넣고 모델을 최적화하는 일이다. 말만 하면 간단한데, 막상 선택지가 쏟아진다. Full fine-tuning 할까 LoRA 할까? Learning rate랑 batch size는? Encoder-decoder에서 길이 다른 sequence는 어떻게 padding 하고, GPU 메모리를 낭비하지 않으면서 터지지 않게 할까? 이 글에서는 한국어 통화 음성에 대해 Whisper large-v3를 학습시킬 때 쓰는 training 설정과, 그 뒤에 있는 공학적 선택(trade-off)을 정리한다.

이 글에서 참고하는 학습 스크립트: train_whisper.py

전처리 데이터에서 Trainer까지

1부가 끝난 시점의 dataset은 Hugging Face Dataset shard 여러 개로 디스크에 있다. 각 샘플은 input_features(미리 계산한 mel spectrogram)와 labels(transcription token ID)를 갖는다. Training script는 학습 중에 raw audio나 feature extractor를 건드리지 않고, 이 shard들을 로드해서 이어 붙인 뒤, padding과 decoder 설정을 처리하는 data collator를 통해 모델에 넣는다.

로딩은 chunk 단위로 이뤄진다. train/·val/ 아래의 chunk_* 디렉터리를 찾아서 load_from_disk로 읽고 concatenate한다. I/O bound 로딩이라 thread pool을 쓰면 shard가 많아도 시작 시간이 부담되지 않는다. 전처리 단계에서 이미 tokenization과 길이 필터링이 끝났기 때문에, “prepare” 단계는 필요한 column이 있는지 확인하는 수준이다 — on-the-fly tokenization은 없다.

Full Fine-Tuning을 쓰는 이유 (그리고 LoRA를 고려할 때)

Encoder·decoder를 전부 업데이트하는 full fine-tuning을 쓰고, LoRA나 QLoRA 같은 parameter-efficient 방법은 쓰지 않았다. 한국어 STT에서는 특정 도메인(통화 음성, 억양, 노이즈 등)과 맞춤법·비유창성에 맞게 적응시키는 게 목표다. Full fine-tuning은 모든 layer를 갱신해서 표현을 바꿀 여지가 크고, 실제로 base model이나 비슷한 compute로 돌린 LoRA 대비 CER/WER 개선이 분명했다.

대신 비용이 든다. Whisper large-v3 full fine-tuning은 GPU 메모리와 multi-GPU 구성이 필요하다. 대안과 쓰는 상황은 대략 다음과 같다.

  • LoRA / QLoRA: 학습 파라미터가 적고 메모리도 적게 써서, 단일 소비자용 GPU나 작은 클라우드 인스턴스로 돌리기 좋다. 빠른 실험, 작은 데이터, 가벼운 적응만 필요할 때 유리하다. 대신 정확도 상한은 full보다 낮은 경우가 많다.
  • Adapter / prefix tuning: 같은 맥락 — 파라미터 적고 빠르고 저렴하지만, domain shift가 클 때는 full fine-tuning만큼의 capacity는 아니다.
  • Full fine-tune: 데이터와 GPU 예산이 충분하고 CER/WER을 최대한 끌어올리고 싶을 때 선택한다. 참고 스크립트는 8× H200(각 143GB) 기준이다. GPU가 적거나 작으면 batch size 줄이고 gradient accumulation으로 effective batch를 비슷하게 맞추면 된다.

정리하면, 정확도 극대화를 위해 full fine-tune을 선택한 것이다. 전처리 파이프라인은 그대로 두고, LoRA/QLoRA용 스크립트로 바꿔서 같은 데이터를 쓸 수도 있다. 모델 wrapper와 optimizer 설정만 바꾸면 된다.

Precision, Checkpointing, Optimizer

학습은 BF16(bfloat16)으로 통일한다. 모델은 torch_dtype=torch.bfloat16, training args에는 bf16=True, data collator에서 padding 끝난 input_features를 forward 전에 bfloat16으로 cast한다. H200(그리고 최근 NVIDIA GPU들)에서는 BF16이 FP16보다 안정성과 속도 면에서 유리하다. FP16은 loss scaling을 신경 써야 하고, 깊은 transformer에서 overflow 위험이 더 크다. BF16이 없는 구형 GPU라면 FP16 + dynamic loss scaling이 대안이다.

Gradient checkpointing을 켜서 backward 시 activation을 다시 계산하고, 저장은 최소화한다. 그 덕분에 메모리를 아껴서 per-device batch size를 크게 잡을 수 있다(참고 설정에서는 GPU당 256). 이걸 끄면 Whisper large-v3를 80GB급 GPU에서 그 batch size로 돌리기 어렵다.

Optimizer는 AdamW이고, fused 구현(adamw_torch_fused)을 쓴다. 최근 GPU에서는 fused kernel이 메모리 접근을 줄여서 기본 AdamW보다 빠르다. Weight decay는 0.01, gradient norm은 1.0으로 clip해서 안정성을 맞췄다.

Learning Rate, Schedule, Batch Size

  • Learning rate: 5e-5. 큰 pretrained 모델 full fine-tuning에서는 과도한 망각 없이 진행할 수 있는 보수적인 값이다. Warmup 끝나고 더 높은 peak LR까지 올리는 식이 아니라, 짧은 warmup(예: step의 5%) 후 이 정도 LR로 유지했다.
  • Scheduler: Cosine decay. 학습 구간 동안 LR이 부드럽게 줄어든다. Linear decay도 자주 쓰이는데, cosine이 마지막 구간에서 조금 더 나은 결과를 준 경우가 많았다.
  • Batch size: 참고 설정은 per-device 256, gradient accumulation 없음 → effective batch = 256 × 8 = 2048. Batch를 크게 하면 BF16에서 안정적이고 step variance가 줄어든다. GPU가 적거나 작으면 per-device batch를 줄이고 gradient accumulation으로 effective batch를 비슷한 구간(예: 1024–2048)에 두면 학습 곡선을 비슷하게 유지할 수 있다.

Data Collator: Padding과 Decoder 설정

Whisper는 encoder-decoder다. Encoder에는 input_features(mel spectrogram), decoder에는 decoder_input_ids를 넣고 나온 logit을 labels와 비교한다. Data collator가 해야 할 일은:

  1. 길이 다른 sequence를 batch 안에서 같은 길이로 padding (예: batch 내 “longest” 기준)해서 tensor로 쌓을 수 있게 하기.
  2. Whisper 최대 길이(스크립트에서는 448 token)를 넘는 label은 truncate해서 shape 에러를 막기.
  3. decoder_input_ids 만들기: label sequence를 한 칸 오른쪽으로 밀고 맨 앞에 decoder start token 넣기. Padding 위치에는 tokenizer의 pad_token_id를 쓰고, labels 쪽 padding 위치에는 **-100**을 넣어서 loss에서 무시되게 하기.
  4. Padding된 **input_features**를 bfloat16으로 cast해서 모델에 넘기기.

참고 스크립트의 collator가 이걸 한 곳에서 처리한다. Padding과 -100 마스킹을 잘못 넣으면, padding을 예측하도록 학습하거나 loss가 padding까지 포함되는 식의 버그가 나기 쉽다. 그래서 이 로직을 collator 한 곳에 모아 두고, dataset 자체는 feature와 label ID만 갖도록 했다.

Evaluation과 Early Stopping

Validation은 500 step마다(설정 가능) 돌린다. Trainer는 predict_with_generate를 켜서 decoder를 greedy decoding(beam 1)으로 끝까지 돌리고, 나온 transcription과 reference를 비교해 CER(character error rate)과 WER(word error rate)를 계산한다. 한국어는 CER를 주 지표로 쓰고, WER은 참고용으로 로깅한다. Eval 시간을 줄이기 위해 validation set 일부(예: 4000개)만 써서 평가한다.

Early stopping은 patience 10 eval step, threshold는 작게 둔다. Trainer는 끝날 때 best model을 로드하도록 되어 있고, 기준은 CER 최소(낮을수록 좋음)다. 그래서 최종 저장 모델은 마지막 step이 아니라 validation CER이 가장 낮았던 checkpoint다 — 곡선이 평평해지거나 overfitting이 시작될 때 중요하다.

Checkpointing과 Resume

Checkpoint는 500 step마다 저장하고, save_total_limit=5로 최근 5개만 유지한다. 스크립트는 output 디렉터리에 기존 checkpoint가 있으면 가장 최근 것부터 이어서 학습한다. 그래서 긴 run이 중간에 크래시나 선점으로 끊겨도, 다시 돌리면 그대로 이어갈 수 있다.

선택 vs 대안 요약

영역 선택 대안
파라미터 업데이트 Full fine-tune LoRA/QLoRA — 적은 연산, 낮은 상한
Precision BF16 구형 GPU에서는 FP16 (loss scaling 필요)
Optimizer AdamW fused 기본 AdamW
LR schedule Cosine + 짧은 warmup Linear, 또는 더 긴 warmup
Batch size 크게 (effective 2048 등) 작게 + gradient accumulation
Eval 지표 CER 주, WER 부 일부 언어는 WER만 쓰기도 함
Best model 끝날 때 CER 기준 best 로드 마지막 checkpoint만 유지

다음 글 예고

학습이 끝나면 best checkpoint가 (예: .../final) 저장된다. 3부에서는 evaluation을 다룬다: 한국어 hold-out test set에서 모델을 돌리고, CER/WER을 재현 가능하게 계산하고, 여러 checkpoint를 data category별로 benchmark하고, baseline과 비교하는 방법.