화자분리를 가장 널리 알려진 Whisper.cpp와 vosk 를 이용해서 간단하게 GUI 프로그램을 만들어 비교를 하면서 느낀것이
그저 모델만 좋은 것보다. 한번 파인튜닝으로 깎아서 진행한다면 어떨까? 라는 의문이 들었고 궁금해졌습니다.
그래서 우선 가볍에 프로토타입 형식으로
한글을 거의 알아듣지 못하는 Tiny 모델을 한국어 경제방송을 학습시키면 한국어를 알아들을까?
라는 가설 그리고 ...
만약 1번의 가설이 참이면, 스몰~라지 등 모델들을 학습시키게 된다면 그럼 성능은 어디까지 올라갈까?
그래서 지금 포스팅부터 도메인 특화 모델을 한번 만들어서 이 한계가 어디까지인지 또 비교 분석해보겠습니다.!!!!

소스코드
https://github.com/uno-km/AMEVA-STT-Trainer
GitHub - uno-km/AMEVA-STT-Trainer: A Whisper-based Korean STT trainer featuring LoRA fine-tuning for high-accuracy speech recogn
A Whisper-based Korean STT trainer featuring LoRA fine-tuning for high-accuracy speech recognition with minimal resources. It offers a full pipeline—from data scraping and audio preprocessing to tr...
github.com
시스템 아키텍처

엔두 2 엔두 파이프라인

런타임 오케스트레이션

Reliability & Resilience

코드 리뷰는 더보기에서!!
-개발자분들은 보면 재미있어 하실거에용
1. 시스템 아키텍처 및 계층 구조 설계
AMEVA STT Trainer는 철저한 계층화(Layered Architecture)를 지향합니다. 각 모듈은 독립적인 책임을 가지며 중앙의 설정 관리자에 의해서만 연결됩니다.
- 데이터 계층 (Data Layer): 유튜브에서 로우 데이터를 수집하고 이를 학습 가능한 청크로 변환하는 복잡한 전처리를 담당합니다.
- 모델 계층 (Model Layer): 허깅페이스의 베이스 모델을 로드하고 LoRA 파라미터를 주입하며 최종적으로 모델을 병합하는 역할을 수행합니다.
- 학습 계층 (Training Layer): 윈도우 환경에 최적화된 스트리밍 방식으로 데이터를 주입하며 실제 가중치 업데이트를 관리합니다.
- 유틸리티 계층 (Utility Layer): 로그 시스템, 예외 처리, 오디오 시그널 프로세싱 등 공통 기능을 제공합니다.
2. 핵심 디자인 패턴: 단일 진실 공급원과 가드
전체 프로젝트를 관통하는 중요한 패턴은 두 가지입니다.
첫째는 Single Source of Truth(SSOT) 패턴입니다.
모든 경로와 하이퍼파라미터는 configs 폴더의 YAML 파일과 src/core/config.py에서 관리됩니다. 코드 어디에서도 하드코딩된 경로를 찾아볼 수 없게 하여 환경 변화에 유연하게 대응합니다.
모든 경로와 설정값을 한곳에서 관리하는 단일 진실 공급원(SSOT)의 구현체입니다.
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
DATASET_DIR = os.path.join(ROOT_DIR, "dataset")
LORA_DIR = os.path.join(ROOT_DIR, "outputs", "lora_adapter")
DEFAULTS = {
"model_id": "openai/whisper-tiny",
"batch_size": 2,
"lora_r": 32,
"wandb": {"enabled": True, "mode": "online"}
}
둘째는 Decorator 패턴을 활용한 전역 예외 관리입니다.
모든 주요 함수에는 @exception_guard라는 데코레이터가 붙어 있습니다. 이는 예외 발생 시 크래시로 끝나는 것이 아니라 에러의 위치와 원인을 로그 파일에 기록하고 시스템을 안전하게 종료하거나 복구 지점을 제안하는 역할을 합니다.
어떤 함수에서 에러가 나더라도 시스템 전체가 침묵 속에 죽지 않도록 방어막을 치는 로직입니다.
def exception_guard(location: str, reraise: bool = False):
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(f"Error at {location}: {str(e)}")
# 에러 로그 기록 후 안전하게 처리
if reraise: raise e
return None
return wrapper
return decorator
이젠 기술적인 알고리즘들 소개입니다. 뭐 코딩테스트에 나오는 그런 알고리즘말고 그냥 문제 해결에서 쓰인 그 알고리즘입니다.
환각 제거기 - 노이즈와 태그의 완전 소거
유튜브 자막의 HTML 태그나 [음악], ♪ 같은 기호들은 STT 모델에게 독약과 같습니다. 소리는 안 나는데 글자만 있으니 AI가 헛소리를 배우게 되거든요.
def clean_text(text: str) -> str:
# <c>, </c> 같은 VTT HTML 태그는 물론
text = re.sub(r"<[^>]+>", "", text)
# [음악], (웃음) 처럼 소리는 안 나는데 적혀 있는 것들
text = re.sub(r"\[.*?\]|\(.*?\)", "", text)
# 심지어 음표 기호(♪)까지 싹 다 지워서 소리와 글자를 1:1로 맞춥니다
text = text.replace("♪", "").replace(" ", " ")
return text.strip()
전문 용어 및 추임새 필터링
특정 도메인의 영상을 다룰 때는 그 사람만의 말습관을 이해해야 합니다. "이제", "막", "그니까" 같은 추임새를 적절히 걷어내야 모델이 핵심 단어에만 집중합니다.
def remove_fillers(text: str) -> str:
# 특유의 다빈도 추임새들
fillers = ["음", "어", "이제", "막", "근데", "그니까", "막말로", "뭐냐", "일단"]
for f in fillers:
# 문장 중간의 의미 없는 추임새만 정교하게 골라냅니다
text = re.sub(rf"\b{f}[,\.]*(?=\s|$)", "", text)
return text.strip()
단위 표준화 - 숫자와 기호의 한글화
AI 모델은 % 보다는 퍼센트, $ 보다는 달러라고 써줬을 때 훨씬 더 잘 알아듣습니다.
def standardize_numbers(text: str) -> str:
# '1,000' -> '1000'으로 쉼표를 제거해 숫자의 연속성을 보장하고
text = re.sub(r"(\d),(\d)", r"\1\2", text)
# 기호를 한글 읽기 방식으로 치환하여 소리와 글자를 일치시킵니다
text = text.replace("%", "퍼센트").replace("$", "달러")
return text
단어 잘림 방지
자막 타임스탬프를 칼같이 자르면 첫 단어나 마지막 단어의 끝부분이 살짝 깎여나가는 경우가 많습니다. "감사합니다"가 "암사합니다"가 되는 비극이죠.
def slice_audio(audio: AudioSegment, start_ms: int, end_ms: int) -> AudioSegment:
# 설정값(100ms)만큼 앞뒤로 패딩을 줘서 단어가 잘리는 걸 물리적으로 방어합니다
pad = CFG["audio_padding_ms"]
s = max(0, start_ms - pad)
e = min(len(audio), end_ms + pad)
return audio[s:e]
윈도우 환경 변수 프리패스
윈도우에서 FFmpeg 설치하고 PATH 잡는 건 초보자들에게는 재앙입니다. 저희는 이를 코드로 해결했습니다.
# C:\ffmpeg\bin\ffmpeg.exe가 있다면 자동으로 환경 변수를 동적으로 할당합니다
FFMPEG_C_PATH = r"C:\ffmpeg\bin\ffmpeg.exe"
if os.path.exists(FFMPEG_C_PATH):
# pydub에게 경로를 직접 가르쳐주고, 시스템 PATH에도 실행 중에만 살짝 추가해주는 센스!
AudioSegment.converter = FFMPEG_C_PATH
os.environ["PATH"] += os.pathsep + os.path.dirname(FFMPEG_C_PATH)
중복자막 병합 알고리즘
유튜브 자막의 겹침 현상을 해결하는 핵심 알고리즘인 Suffix-Prefix Matching입니다.
def merge_texts(text1, text2):
max_overlap = min(len(text1), len(text2))
for i in range(max_overlap, 1, -1):
if text1.endswith(text2[:i]):
return text1 + text2[i:]
return text1 + " " + text2
윈도우용 스트리밍 데이터 주입
메모리 맵핑 에러를 회피하기 위해 제너레이터 방식을 사용한 IterableDataset 구현부입니다.
def dataset_generator(audio_list, transcription_list, model_id):
# local_files_only=True로 오프라인 보장
fe = WhisperFeatureExtractor.from_pretrained(model_id, local_files_only=True)
tk = WhisperTokenizer.from_pretrained(model_id, local_files_only=True)
for audio, trans in zip(audio_list, transcription_list):
# 실시간으로 하나씩 읽어서 전처리 후 양도(yield)
processed = _pure_prepare_fn({"audio": audio, "transcription": trans}, fe, tk)
yield processed
모델 병합 및 오프라인 배포 최적화
학습된 가중치를 합치고 외부 런타임 호환성을 위해 vocab을 강제 저장하는 부분입니다.
def merge_and_save():
base_model = WhisperForConditionalGeneration.from_pretrained(model_id, local_files_only=True)
peft_model = PeftModel.from_pretrained(base_model, LORA_DIR)
# 가중치 융합 후 저장
merged = peft_model.merge_and_unload()
merged.save_pretrained(MERGED_DIR)
# whisper.cpp 등 외부 런타임을 위한 보캐러리 강제 생성
processor.tokenizer.save_vocabulary(MERGED_DIR)
3. 데이터 관리 및 디렉터리 아키텍트
별도의 데이터베이스 대신 파일 시스템을 데이터베이스처럼 활용하는 계층적 저장 구조를 택했습니다.
수만 개의 오디오 청크를 효율적으로 관리하기 위해 YYYY/MM/DD/VideoID 형태의 디렉터리 구조를 자동 생성합니다.
이는 운영체제의 파일 시스템 부하를 최소화하고 데이터 검색 속도를 높이기 위한 설계입니다. 모든 경로는 상대 경로로 관리되어 프로젝트 폴더를 어디로 옮기든 즉시 동작하는 높은 이식성을 자랑합니다.
계층 구조 생성
한 폴더에 파일이 만 개 이상 쌓이면 윈도우의 NTFS 파일 시스템은 인덱싱 속도가 급격히 느려집니다. 우리는 이를 방지하기 위해 날짜 기반 계층화를 자동화했습니다.
def date_to_folder(date_str: str, video_id: str) -> str:
# 20260511 같은 날짜 형식을 2026/05/11/video_id 구조로 분해합니다.
year, month, day = date_str[:4], date_str[4:6], date_str[6:8]
# os.path.join을 써서 윈도우(\)와 리눅스(/) 경로 호환성까지 챙깁니다.
target_path = os.path.join(DATASET_DIR, year, month, day, video_id)
# makedirs(exist_ok=True)로 폴더가 이미 있어도 에러 없이 조용히 넘어가는 센스!
os.makedirs(target_path, exist_ok=True)
return target_path
폴더 하나당 파일 숫자를 수십 개 수준으로 제한하여, 파일 탐색 및 읽기 속도를 상수 시간에 가깝게 유지했습니다.
상대 경로 관리
우리는 C:\...로 시작하는 절대 경로를 코드 어디에도 저장하지 않습니다. 모든 경로는 프로젝트 루트를 기준으로 계산됩니다.
# metadata.csv에 저장할 때의 로직
for chunk in chunks:
# os.path.relpath를 써서 DATASET_DIR 기준의 상대 경로를 추출합니다.
# 예: "2026/05/11/video1/chunks/chunk_001.wav"
rel_path = os.path.relpath(chunk_full_path, DATASET_DIR)
# CSV에는 이 상대 경로만 저장합니다.
writer.writerow([rel_path, transcript_text])
이렇게 관리하면 프로젝트 폴더를 USB에 담아 다른 컴퓨터로 옮겨도 환경 변수나 설정 변경 없이 즉시 학습이 가능합니다. 데이터셋 자체가 하나의 'Portable' 모듈이 되는 것이죠.
"데이터 무결성" 덮어쓰기 방지
동일한 영상을 다시 수집하더라도 기존에 공들여 정제한 청크 파일들이 날아가지 않도록 방어 로직을 심었습니다.
# chunk_path가 이미 존재하고 크기가 0보다 크면 작업을 생략합니다.
if os.path.exists(chunk_path) and os.path.getsize(chunk_path) > 0:
# 이미 정제된 파일은 건드리지 않고 넘어가서 데이터의 영속성을 보장합니다.
continue
중복 작업을 방지하여 전처리 시간을 단축하고, 수동으로 편집한 데이터가 있다면 이를 보호하는 일종의 '파일 기반 캐싱' 전략입니다.
결국 이 설계는 가장 단순한 도구(파일 시스템)를 가장 똑똑하게 쓰는 법을 보여줍니다.
거창한 DB 서버 없이도 데이터의 무결성, 관리의 편의성, 그리고 시스템의 성능까지 모두 잡은 실전형 아키텍처라고 할 수 있습니다!
4. 파이프라인 설계와 워크어라운드의 미학
노트북 CPU 환경이라는 제약 조건을 극복하기 위해 기술적인 우회로를 곳곳에 심었습니다.
가장 대표적인 것이 WinError 87을 회피하기 위한 IterableDataset 기반의 스트리밍 설계입니다. 메모리 맵핑 대신 파이썬 제너레이터를 사용하여 RAM 사용량을 상수로 고정시켰습니다.
또한 whisper.cpp로의 변환을 염두에 두고 병합 과정에서 vocab.json 파일을 강제로 생성하는 로직을 포함했습니다.
이는 표준 라이브러리가 놓치기 쉬운 배포 단계의 호환성을 엔지니어링적으로 해결한 대목입니다.
윈도우에서 수만 개의 오디오 파일을 한꺼번에 Dataset.map으로 처리하려고 하면 커널 레벨에서 WinError 87이 터집니다.
메모리 맵핑(Memory Mapping) 한계를 초과하기 때문이죠.
def dataset_generator(audio_list, transcription_list):
# 데이터를 한꺼번에 올리지 않고, 하나씩 '양도(yield)'합니다.
for audio, trans in zip(audio_list, transcription_list):
# 학습 루프가 "다음 데이터 줘!" 할 때만 로딩과 전처리가 일어납니다.
processed = _pure_prepare_fn({"audio": audio, "transcription": trans}, fe, tk)
yield processed
# 이 제너레이터를 HuggingFace IterableDataset으로 감싸면 끝!
train_dataset = IterableDataset.from_generator(
dataset_generator,
gen_kwargs={"audio_list": audios, "transcription_list": trans}
)
이렇게 하면 데이터가 10만 개가 되어도 RAM 사용량은 상수가 됩니다. 윈도우 커널을 속이고(?) 무한대의 데이터를 학습시킬 수 있는 비결이죠.
전처리 도구 로컬 초기화
윈도우에서 멀티프로세싱을 쓸 때 PickleError가 나는 고질적인 문제가 있습니다. 특히 FeatureExtractor 같은 객체들을 전역으로 선언하면 자식 프로세스로 복사가 안 되죠.
def dataset_generator(model_id, ...):
# 함수 외부(전역)가 아닌, 제너레이터 '내부'에서 도구들을 초기화합니다.
# 이렇게 하면 각 프로세스가 자기만의 도구를 가져서 직렬화(Pickle) 에러가 원천 봉쇄됩니다.
fe = WhisperFeatureExtractor.from_pretrained(model_id, local_files_only=True)
tk = WhisperTokenizer.from_pretrained(model_id, local_files_only=True)
# 이제 안전하게 병렬 전처리가 가능합니다.
윈도우의 spawn 방식에 대한 깊은 이해를 바탕으로, 구조를 살짝 비틀어 시스템 안정성과 병렬 처리 속도를 모두 잡은 설계입니다.
5. 로그 기능 및 대시보드 엔지니어링
단순히 텍스트를 출력하는 로깅을 넘어 시스템의 생존 신호를 시각화하는 데 집중했습니다.
psutil을 연동한 실시간 자원 모니터링 시스템은 CPU 부하에 따라 하단 바의 색상을 녹색에서 빨간색으로 가변시킵니다.
이는 개발자가 학습의 진행 상태를 글자가 아닌 색상만으로도 직관적으로 파악할 수 있게 돕습니다.
모든 로그는 터미널 출력과 동시에 날짜별 로그 파일에 기록되어 추후 디버깅의 핵심 자산이 됩니다.
STT 파인튜닝을 위한 파이프라인
먼저 STT 파인튜닝을 위해 아래와 같이 계획을 잡았습니다.

- 데이터 샘플링 및 코퍼스 정제 파이프라인
- 데이터 학습
- 검증 및 배포
1. 데이터 샘플링 및 코퍼스 정제 파이프라인
이번 단계의 목표는 유튜브의 비정형 스트리밍 데이터로부터 음성 인식 모델 학습에 딱 맞는 '고순도 데이터셋'을 구축하는 것이었습니다.
이를 위해 다단계 신호 처리와 언어적 정제 알고리즘을 촘촘하게 적용했는데요, 구체적으로 어떤 작업을 했는지 정리해 드리겠습니다.
1. 자동화된 데이터 획득
먼저 yt-dlp 라이브러리를 활용해 타겟 도메인(유튜브 채널)의 최신 영상에서 오디오 스트림과 VTT 자막을 동시에 추출했습니다.
이때 모든 오디오 소스는 추출 단계부터 16,000Hz 샘플링 레이트와 단일 채널로 변환되도록 설정했습니다.
Whisper 모델의 인코더가 요구하는 사양을 물리적으로 딱 맞춰서 시작한 셈이죠.
2. 언어적 정제와 중복 제거
유튜브 자동 생성 자막을 그대로 쓰면 문장이 계속 겹쳐서 나오는 '누적형 중복' 문제가 발생하는데요. 이를 해결하기 위해 저희만의 독자적인 병합 로직을 넣었습니다.
Suffix-Prefix Overlap Matching
merge_texts 함수를 통해 이전 문장의 끝과 다음 문장의 앞부분이 겹치는 구간을 찾습니다. 최소 2글자 이상 일치하면 이를 하나의 문장으로 깔끔하게 합쳐버려요. 이렇게 하면 모델이 똑같은 말을 반복해서 배우는 데이터 오염을 완벽히 차단할 수 있습니다.
사실 처음엔 유튜브가 주는 자막을 그대로 쓰면 될 줄 알았습니다. 하지만 데이터를 직접 훝어보니 두 가지 치명적인 문제가 발견되었고, 이를 해결하기 위해 저희만의 방식을 도입하게 되었습니다.ㅠㅠ
자막 도배(스크롤링) 현상 제거
발견: 유튜브 자동 생성 자막(ASR) 추출 시, 이전 발화 내용이 다음 자막에 누적되어 출력되는 스크롤링 현상 발견.
문제: 해당 데이터로 모델 학습 진행 시, 중복된 텍스트로 인해 AI가 발화를 심하게 더듬거나 특정 토큰을 반복하는 할루시네이션 학습 편향 발생 우려. 일반적인 텍스트 병합 라이브러리로는 원천적인 해결 불가.
처리: 앞 문장의 후위와 뒷 문장의 전위를 글자 단위로 대조하여 겹치는 구간을 정확히 도려내는 Suffix-Prefix Matching 알고리즘 자체 구현 및 적용 완료. 텍스트 중복률 0% 달성.
Regex-based Sanitization:
비발화 메타데이터(환각 유발 토큰) 소거 를 통해 clean_text 함수로는 [음악], (웃음) 같은 비언어적 요소나 HTML 태그를 정규표현식으로 싹 지웠습니다. 모델이 목소리와 글자 사이의 관계에만 집중할 수 있게 정제한 것이죠.
발견: 원시 VTT 파일 내부에 음악, 웃음 등의 상황 묘사 텍스트 및 HTML 태그 다수 포함 확인.
문제: 실제 발화되지 않은 무음 구간이나 효과음 구간에 텍스트가 맵핑될 경우, 모델이 소리와 무관한 텍스트를 출력하도록 오학습됨. 이는 저사양 환경에서 모델 정확도를 심각하게 훼손하는 주원인으로 지목됨.
처리: 정규표현식을 활용하여 대괄호, 소괄호, 특수 기호 등으로 감싸진 모든 비발화 메타데이터를 전처리 파이프라인에서 완전 소거. 음성 신호와 텍스트 간의 1:1 매핑 무결성 확보 완료.
3. 문맥 기반 시계열 정렬 알고리즘
Whisper와 같은 Sequence-to-Sequence 기반 음성 인식 모델은 고정된 시간 윈도우(Time Window, 일반적으로 30초) 내에서 오디오 텐서와 텍스트 시퀀스의 상관관계를 학습합니다.
이때 단순히 기계적인 시간 단위로 오디오를 절단할 경우 심각한 모델 성능 저하가 발생합니다. 저는 이 문제를 해결하기 위해 문맥 경계 보호 알고리즘을 도입했습니다.
문장 경계 보호 전략
문제 : 오디오 청크를 30초 등 특정 시간에 도달했다고 기계적으로 자를 경우, "오늘 우리는 경제에 대해서 알아볼 거..."와 같이 발화 중간에 시퀀스가 강제 종료됩니다.
이는 모델이 문장의 종결 형태와 문법적 구조를 학습하는 것을 방해하며, 추론 시 불완전한 문장을 생성하는 할루시네이션을 유발합니다.
해결 및 구현: 이를 방지하기 위해 텍스트 전처리 단계에서 마침표(.), 물음표(?), 느낌표(!) 등의 문장 종결 부호를 실시간으로 감시하는 로직을 구현했습니다.
설정된 청크 임계 시간에 도달하더라도 즉시 절단하지 않고, 논리적인 문장이 끝날 때까지 유동적으로 대기하여 문맥의 완결성을 보존했습니다.
동적 윈도우 크기 최적화
문제 분석: 극단적으로 짧은 데이터(예: 1~2초 분량의 "네", "그렇죠")가 다수 포함되면 문장 단위의 억양이나 장기 문맥을 학습할 수 없습니다.
반면, 데이터가 30초를 초과하면 Whisper 모델의 텐서 처리 한계를 넘어 후반부 시퀀스가 소실되거나, 어텐션 연산량이 폭증하여 CPU 로컬 환경에서 Out Of Memory(OOM)가 발생할 수 있습니다.
해결 및 구현: 이에 따라 15초에서 30초 사이를 'Golden Window'로 설정했습니다. 모델이 충분한 문맥 정보를 파악할 수 있는 최소 길이(15초)와, CPU 연산 및 메모리 한계를 초과하지 않는 최대 길이(30초) 사이에서 문장 종결 부호에 맞춰 유동적으로 청크를 생성하도록 알고리즘을 최적화했습니다.
이는 단순한 오디오 분할이 아닌, 모델의 어텐션 메커니즘이 인류의 자연스러운 발화 호흡을 효율적으로 매핑할 수 있도록 돕는 핵심 데이터 엔지니어링 전략입니다.
4. 오디오 시그널 엔지니어링
텍스트 정제와 병행하여, 음성 신호의 물리적 편차를 통제하고 모델의 어텐션 연산 효율을 극대화하기 위한 시그널 프로세싱을 적용했습니다.
진폭 정규화
발견 및 문제: 수집된 유튜브 오디오 소스는 영상별 녹음 환경과 마이크 세팅에 따라 데시벨 편차가 극심하게 나타났습니다.
입력 신호의 진폭이 불균일할 경우, 신경망의 기울기가 요동쳐 로스 수렴 속도가 저하되고 과적합을 유발할 위험이 발견되었습니다.
해결 및 구현: 이를 통제하기 위해 전처리 파이프라인에 Peak Normalization 알고리즘을 적용했습니다.
모든 오디오 청크의 최대 진폭을 사전 정의된 타겟 레벨로 스케일링하여 신호 크기를 규격화했습니다.
이를 통해 모델이 음량의 물리적 크기가 아닌 음성 고유의 주파수 패턴 추출에만 연산 자원을 집중할 수 있는 환경을 구축했습니다.
음성 활동 감지 기반 묵음 제거
발견 및 문제: 생성된 오디오 청크의 전후단에 평균 1~3초가량의 의미 없는 묵음 구간이 포함되어 있음을 확인했습니다.
이는 연산 자원이 극도로 제한된 CPU 로컬 환경에서, 전체 텐서 연산량의 약 20~30%를 무의미한 제로 패딩 처리에 낭비하게 만드는 심각한 병목 요인으로 지목했습니다.
해결 및 구현: 이 연산 낭비를 제거하기 위해 trim_silence 함수에 Voice Activity Detection(VAD) 기법을 도입했습니다.
에너지 임계값(Energy Threshold)을 설정하여 발화가 없는 정적 구간을 정밀하게 절삭했습니다.
이 조치를 통해 100%에 가까운 유효 발화만으로 구성된 고밀도 텐서를 생성함으로써, 모델의 단위 시간당 정보 처리 밀도를 극대화하고 CPU의 한계 성능을 돌파하고자 했습니다.
5. 확장성과 무결성을 고려한 데이터셋 아키텍처 설계
모델 학습에 필요한 데이터는 단순한 파일의 집합이 아닙니다. 수만 개 단위로 증식하는 데이터의 생명주기를 관리하고, 환경 변화에 독립적으로 동작할 수 있는 파일 시스템 계층 구조를 설계했습니다.
OS 병목을 방지하는 계층적 분산 스토리지
발견 및 문제: 단일 디렉토리에 수만 개의 청크 파일이 집중될 경우, Windows 파일 시스템(NTFS)의 I/O 처리량이 급감하고 디렉토리 스캔에 심각한 병목(Bottleneck) 현상이 발생함을 확인했습니다.
이는 데이터 로딩 속도 저하를 넘어 OS 커널 레벨의 프리징을 유발할 수 있습니다.
해결 및 구현: 이를 방지하기 위해 단일 구조를 버리고, 유튜브 영상의 업로드 메타데이터를 파싱하여 dataset/YYYY/MM/DD/{video_id}/chunks/ 형태의 시간 기반 계층적 분산 구조를 구현했습니다.
이 분산 아키텍처를 통해 디렉토리당 파일 개수를 OS가 쾌적하게 처리할 수 있는 임계치 이하로 유지하여 파일 시스템 부하를 원천 차단했습니다.
환경 비의존적 파이프라인 구축
발견 및 문제: 메타데이터(metadata.csv)에 오디오 파일의 위치를 절대 경로로 기록할 경우, 프로젝트의 실행 환경(다른 PC, 클라우드, 도커 컨테이너 등)이 변경될 때마다 경로 충돌이 발생하여 전체 데이터셋을 재구축해야 하는 치명적인 호환성 결함이 존재했습니다.
해결 및 구현: 이를 완벽히 해결하기 위해 메타데이터 내의 경로 정보를 프로젝트 루트 기준의 상대 경로로 전면 전환했습니다.
데이터 로딩 시 런타임 환경의 최상위 경로를 동적으로 결합하도록 설계했습니다. 이로써 어떤 OS나 컨테이너 환경으로 이관되더라도 즉각적인 파인튜닝이 가능한 데이터셋 체계를 완성했습니다.
2. 데이터 학습
고가의 GPU 서버가 없어도 학습은 가능하도록 설계했습니다.
저는 효율과 안정성이라는 두 마리 토끼를 잡기 위해 네 가지 핵심 전략을 학습 엔진에 심었습니다.
2.1 파라미터 효율적 전이 학습 (PEFT) 기반 LoRA 아키텍처 도입
본 파이프라인은 로컬 CPU 환경에서의 Fine-Tuning을 목표로 설계되었습니다. 수억 개의 파라미터를 가진 Whisper 모델의 Full Fine-Tuning은 제한된 메모리 환경에서 불가능하므로, 파라미터 효율적 전이 학습(PEFT, Parameter-Efficient Fine-Tuning) 기법 중 하나인 LoRA(Low-Rank Adaptation)를 핵심 엔진으로 채택했습니다.
저차원 행렬 분해를 통한 VRAM 최적화
발견 및 문제: Whisper 모델의 모든 사전 학습된 가중치의 기울기를 연산하고 업데이트할 경우, CPU 환경에서는 필연적으로 Out-of-Memory(OOM)가 발생합니다.
해결 및 구현: 원본 가중치 행렬을 동결하고, 어텐션 레이어 내부에 병렬로 연결된 학습 가능한 저차원 행렬만을 추가했습니다.
이 접근법을 통해 전체 모델 파라미터의 약 1% 미만만을 학습 가능 상태로 전환하여, 메모리 점유율을 1/10 수준으로 극적으로 감소시키고 CPU 환경에서의 안정적인 학습을 구현했습니다.
지식 보존 및 과적합 방지
발견 및 문제: 특정 도메인(경제 용어 등)의 코퍼스에만 모델을 과도하게 노출시킬 경우, 기존 모델이 보유한 범용 한국어 언어 모델링 능력이 소실되는 치명적 망각현상 발생 위험이 높습니다.
해결 및 구현: LoRA 아키텍처는 원본 파라미터를 동결하므로, 범용 음성 인식 능력에 해당하는 베이스 지식을 안전하게 보존합니다.
새롭게 학습되는 도메인 특화 지식은 추가된 저차원 어댑터 가중치에만 독립적으로 인코딩되므로, 범용 성능 저하 없이 타겟 도메인의 어휘 인식률만을 정밀하게 향상시키는 최적화된 편향 학습이 가능합니다.
모델 모듈화 및 배포 효율성
해결 및 구현: 학습 결과물은 수 기가바이트(GB) 크기의 전체 체크포인트가 아닌, 수 메가바이트 수준의 어댑터 가중치 파일형태로 출력됩니다.
이는 스토리지 I/O 비용을 절감하며, 추론 단계에서 베이스 모델과의 병합 처리를 통해 신속한 배포 및 GGUF 변환 파이프라인과의 즉각적인 연동을 보장합니다.
2.2 윈도우 커널 한계 극복을 위한 스트리밍 데이터 로더 구현
윈도우에서 대용량 데이터를 학습시킬 때 가장 무서운 건 WinError 87 같은 메모리 맵핑 에러입니다.
따라서 데이터를 메모리에 한꺼번에 올리지 않고, 학습 루프가 돌아갈 때마다 실시간으로 데이터를 조금씩 읽어오는 스트리밍 방식을 채택했습니다.
덕분에 데이터가 아무리 늘어나도 RAM 점유율이 일정하게 유지되어, 갑자기 프로그램이 뻗어버리는 불상사를 완벽히 차단했습니다.
그런데 왜 스트리밍으로 선택한 이유는 계속 오류가 났었는데요... 보통 딥러닝을 할 때는 모든 데이터를 메모리에 미리 올려두고 학습하는 방식을 선호합니다.
속도가 빠르니까요.
하지만 저 처럼 윈도우 노트북에서 작업할 때는 이 방식이 치명적인 독이 될 수 있습니다.
메모리 맵핑 한계 및 시스템 에러 해결
발견 및 문제: 대규모 오디오 데이터를 윈도우 로컬 환경에서 일괄 메모리에 적재할 경우, 파일 시스템의 한계로 인해
[WinError 87] 매개 변수가 틀립니다
예외가 발생하여 훈련 프로세스가 강제 종료되는 현상 발견.
해결 및 구현: 데이터를 메모리에 일괄 적재하는 정적 데이터셋 방식을 폐기했습니다.
훈련 루프가 데이터를 요구하는 시점에 맞추어 디스크에서 단일 데이터 단위로 실시간 읽기를 수행하는 스트리밍 방식의 데이터 로더 아키텍처로 전면 개편했습니다.
제너레이터 기반 지연 평가 로직 적용
발견 및 문제: 정적 로딩은 연산 속도 측면에서 유리하나, 자원 제약이 있는 환경에서는 즉각적인 시스템 자원 고갈 및 캐시 병목을 유발합니다.
해결 및 구현: 제너레이터 패턴을 활용하여 훈련 루프의 호출 시점에만 오디오 파일을 로드하고 전처리를 수행하는 지연 평가 로직을 구현 완료했습니다.
시스템 자원 점유율 고정 및 안정성 확보
스트리밍 방식을 적용한 결과, 입력 데이터의 총규모 증가와 무관하게 시스템 RAM 점유율이 일정한 수준(약 5GB 미만)으로 고정됨을 확인했습니다.
윈도우 운영체제 고유의 메모리 할당 병목 현상을 원천 차단하여 시스템 셧다운 없이 장시간 훈련 파이프라인 가동을 완료했습니다.
2.3 자동 복구 시스템
로컬 런타임에서 대규모 모델 훈련 시 발생하는 예기치 못한 인터럽트(OS 자동 업데이트, 열 스로틀링, OOM 크래시 등)로부터 학습 파이프라인의 영속성을 보장하기 위한 안전장치를 설계했습니다.
상태 기반 체크포인트 스캐닝
발견 및 문제: 장시간의 CPU 훈련 도중 시스템이 셧다운될 경우, 기존의 수동 복구 방식은 엔지니어가 직접 최종 성공 스텝을 확인하고 인자를 재설정해야 하는 운영 오버헤드가 발생했습니다.
해결 및 구현: 훈련 진입점에 파일 시스템 스캐닝 로직을 내장했습니다. 런타임 초기화 시 outputs/lora_adapter 내의 checkpoint-XXXX 디렉토리 목록을 파싱하여 가장 높은 정수 인덱스를 추출합니다.
이 인덱스를 기반으로 resume_from_checkpoint 파라미터를 동적으로 바인딩하여, 사람의 개입 없이 최신 상태에서 훈련을 즉각 재개(Auto-Resume)하는 자가 복원 시스템을 구축했습니다.
스트리밍 데이터 로더와의 동기화
발견 및 문제: 정적 데이터셋이 아닌 IterableDataset을 사용할 경우, 복구 시점에 데이터셋의 판독 위치를 추적하지 못하면 이미 학습된 데이터를 중복 소진하거나 필수 데이터를 건너뛰는 불일치가 발생합니다.
해결 및 구현: 추출된 최종 체크포인트 스텝 수에 배치 사이즈와 경사 누적 스텝을 승산하여 절대 데이터 인덱스를 산출하는 로직을 데이터 제너레이터 내부에 구현했습니다.
이를 통해 시스템 재시작 시, 중복 계산 없이 해당 인덱스까지의 데이터 추출을 신속하게 건너뛰는'스트리밍 지연 복구 메커니즘을 완성했습니다.
완전 오프라인 로딩
학습 도중 허깅페이스 서버에 접속하려다 네트워크 문제로 학습이 멈추는 건 정말 끔찍한 일이었습니다.... 완전 오프라인인줄알고 노트북 다 뽑고 집에 가는길에 학습이 안되길래 진짜 절망을 느꼈었습니다.
알고보니 인터넷이 있어야만 학습을 할 수 있었습니다.... 물론 인터넷이 재연결되면서 다시 돌았지만요 진짜 ...
그래서 모든 모델 로딩 로직에 local_files_only=True 옵션을 걸었습니다.
100% 로컬 환경을 보장하여 학습의 독립성과 보안성을 확보하기 위해서였습니다. 이제 이 시스템은 인터넷 선을 뽑아도 수집한 데이터로 모델을 깎아낼 수 있는 '독립된 AI 공장'이 되었습니다.
3. 모델 검증 및 배포
학습이 완료된 모델은 아직 '어댑터'라는 별도의 옷을 입고 있는 상태입니다. 이를 진짜 하나로 합치고, 실전에 투입하기 위한 최적화 과정을 거쳤습니다.
3.1 정량적 검증 및 교차 평가
우리는 설계한 엔드투엔드 파이프라인의 실효성을 입증하기 위해 주관적인 정성 평가를 배제하고, evaluate 및 jiwer 라이브러리를 활용하여 객관적인 정량 지표 기반의 검증 프레임워크를 구축했습니다.
학습에 한 번도 사용되지 않은 완전한 형태의 새로운 도메인 영상을 데이터셋으로 삼아, 베이스라인인 순정 whisper-tiny 모델과 우리가 파인튜닝한 모델의 전사 결과를 나란히 대조하는 교차 평가를 수행합니다.
모델 병합
학습된 LoRA 어댑터는 베이스 모델과 합쳐져야 진정한 힘을 발휘하며 merge_and_unload()라는 기술을 써서, 둥둥 떠 있던 학습 가중치들을 베이스 모델의 신경망 안으로 완전히 흡수시켰습니다.
이렇게 해야 별도의 로딩 라이브러리 없이도 어디서든 일반 Whisper 모델처럼 사용할 수 있기 때문입니다. 이제 이 모델은 세상에 단 하나뿐인 슈카 특화 독립 모델이 되었습니다.
GGUF 텐서 변환 및 종속성 주입
모델의 가중치를 C/C++ 기반의 whisper.cpp 엔진이 직접 로드하고 연산할 수 있는 고성능 텐서 포맷으로 변환하여, CPU 및 모바일 AP 연산에 최적화된 추론 아키텍처를 확보합니다.
변환 과정에서 whisper.cpp의 포맷 컨버터가 허깅페이스 표준 tokenizer.json 대신 구형 규격인 vocab.json을 필수적으로 요구하는 병목이 확인되었습니다. 이를 해결하기 위해 파이썬 파이프라인 내부에서 모델 객체를 래핑하여 vocab.json 및 added_tokens.json 메타데이터를 강제로 생성 및 주입하는 무결성 스크립트를 구현했습니다.
이를 통해 변환기의 에러 발생을 원천 차단했습니다.
에코시스템 통합 및 실전 배포
배포 구조: GGUF 변환이 완료된 최종 .bin 아티팩트는 시스템 전역에서 접근 가능한 공통 모델 저장소에 떨어지게 했습니다.
이로써 이전 포스팅에서 설계한 추론 전용 GUI 툴인 'AMEVA-STT-Agent'가 본 파이프라인에서 갓 생산된 도메인 특화 모델을 즉시 로드하여 벤치마크 및 실무 추론을 수행할 수 있게 되었습니다.
이는 외부 API나 클라우드 자원에 전혀 의존하지 않고, '학습 데이터 수집-정제-모델 파인튜닝-양자화 변환-GUI 추론'에 이르는 완전한 온프레미스(On-Premise) AI 에코시스템이 완성되었음을 의미합니다!!!!

다음 포스팅으로 이렇게 개발한 모델학습머신을 통해 Tiny 모델을 학습시키고 기존 Tiny 모델과 학습된 Tiny 모델을 비교해서
가설1 : 한국어도 못들어먹는 처참한 Tiny 모델도 학습하면 한국어 말귀는 알아듣는다.
파헤쳐 보겠습니다. 그리고 다음 포스팅이후로는 모델을 직접 학습해보고
학습량과 모델간의 상관성을 한번 분석하고 비교 검증해보는 포스팅으로 찾아 뵙겠습니다.
긴 글 읽어주셔서 감사합니다.!!!

댓글