ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Whisper 디코더에 어휘를 박아 넣기 — initial_prompt 한 줄의 효과
    IT 2026. 5. 10. 23:00
    Whisper 디코더에 어휘를 박아 넣기 — initial_prompt 한 줄의 효과

    모델을 turbo로 키우고 beam_size를 5로 올린 다음에도, 어떤 단어는 끝까지 잘 안 들렸습니다. 아이가 자주 말하는 "마인크래프트", "로블록스", "선생님 이름" 같이 일상어와 다른 어휘에서 그랬습니다.

    모델이 더 크고 빔 후보가 더 많아도, 디코더가 떠올릴 수 있는 "그럴 법한 단어"의 분포 자체가 바뀌지 않으면 한계가 있습니다. 같은 음향 신호라도 디코더가 "이 단어가 나올 가능성이 높다"고 미리 알고 있으면 어휘 매칭이 훨씬 정확해집니다. Whisper에는 그걸 살짝 밀어주는 initial_prompt라는 인자가 있습니다.

    initial_prompt이 뭘 하는가

    Whisper의 디코더는 매 스텝에서 다음 토큰을 확률 분포로 예측합니다. initial_prompt는 이 예측 직전에 디코더의 입력 시퀀스 앞쪽에 끼워 넣는 텍스트입니다. 디코더 입장에서는 "이미 이런 문맥이 있었다"고 가정한 상태에서 다음 토큰을 뽑게 됩니다.

    효과는 두 가지입니다. 첫째, 어휘 편향입니다. 프롬프트 안에 있는 단어 또는 그것과 비슷한 단어가 다음 토큰 후보로 선택될 확률이 살짝 올라갑니다. 둘째, 스타일 편향입니다. 프롬프트가 정중체면 디코더도 정중체로 변환하는 경향이 있고, 반말체면 반말로 떨어집니다.

    주의할 점은 이 효과가 강제는 아니라는 것입니다. 디코더는 음향 신호 자체의 정보를 우선합니다. 프롬프트는 "동률이거나 미세한 차이일 때 한쪽으로 살짝 밀어주는" 정도로 작용합니다. 그래서 길거나 어색한 프롬프트는 오히려 인식을 헛길로 가게 만듭니다.

    initial_prompt 적용 전후의 디코더 다음 토큰 확률 분포

    그림 설명 — 같은 음성 신호("마인크"라고 발음한 토막)를 같은 모델로 디코딩할 때, initial_prompt 적용 여부에 따라 다음 토큰 후보의 확률 분포가 어떻게 달라지는지 개념적으로 보여줍니다. 왼쪽(prompt 없음): "래"가 1등이긴 하지만 0.32로 약하고 "림"(0.27)과 격차가 작아서 잡음에 자주 빗나갑니다. 오른쪽(prompt 적용): "래"가 0.61로 압도적이 됩니다. 디코더가 prompt 안에서 "마인크래프트"라는 단어 패턴을 본 적이 있어서 같은 발음 단편을 그쪽으로 잡아당기는 것입니다.

    어떤 어휘를 넣어야 하는가

    가장 효과적인 prompt는 화자가 자주 쓰지만 모델이 잘 모르는 어휘입니다. 인터넷 데이터로 학습된 Whisper는 일반적인 한국어는 잘 알지만, 게임 이름·아이 친구 별명·반려동물 이름·자주 가는 학원 이름 같은 좁은 도메인 어휘는 약합니다. 그런 어휘를 prompt에 몇 개 끼워주는 것이 가장 가성비가 좋습니다.

    제 경우엔 다음 정도를 prompt로 두었습니다.

    • 대화 상대(아이) 본인의 이름과 가족 호칭
    • 일상 주제어: 학교, 친구, 게임, 책, 숙제
    • 아이가 자주 입에 올리는 게임 고유명사 두세 개

    합치면 한국어 한 줄짜리 문장입니다. Whisper의 prompt는 최대 224 토큰이라는 제한이 있는데, 한국어는 토큰 효율이 영어보다 낮아 100~150자 정도면 그 한도에 닿습니다.

    이 부분이 헷갈리기 쉽습니다. 영어에서는 흔히 "1 토큰 ≈ 4글자" 정도이고, 그래서 글자 수가 토큰 수보다 큽니다. 그런데 한국어는 정반대예요. Whisper가 쓰는 BPE 토크나이저는 영어 학습 데이터가 압도적이라, 자주 등장한 영어 단어는 통째로 한 토큰으로 묶이지만, 상대적으로 적게 본 한글 문자는 한 글자가 UTF-8 바이트 단위로 분해되어 평균 1.5~2 토큰이 되는 경우가 많습니다. 결과적으로 한글 100자가 토큰 150~200개를 잡아먹습니다. "한국어로 대략 100~150자"라는 어림 수치는 여기서 나옵니다.

    그래서 한국어 prompt는 머릿속 직관보다 더 빨리 한도에 닿습니다. 너무 길게 넣으면 그 안의 단어들끼리 영향이 섞여서 효과가 흐려지고, 음향 신호와 무관한 어휘가 강제 편향으로 작용해 오히려 인식이 망가지는 경우도 있습니다. 짧게, 화자 어휘에 가깝게가 핵심입니다.

    적용 — 환경변수 + transcribe 인자

    저는 prompt를 코드에 박지 않고 환경변수로 빼두었습니다. 그러면 가족 구성·관심사 변화에 따라 systemd 유닛 파일만 고쳐서 반영할 수 있습니다.

    # whisper_daemon.py
    # 화자 어휘를 디코더에 편향. 짧게 (≤224 토큰).
    INITIAL_PROMPT = os.environ.get("WHISPER_INITIAL_PROMPT", "")
    
    
    def transcribe_blocking(audio_path: str) -> str:
        model = load_model()
        result = model.transcribe(
            audio_path,
            language=LANG,
            beam_size=5,
            fp16=(DEVICE == "cuda"),
            condition_on_previous_text=False,
            initial_prompt=INITIAL_PROMPT or None,
        )
        return (result.get("text") or "").strip()
    

    코드 설명 — 환경변수 WHISPER_INITIAL_PROMPT를 읽어 모듈 전역 상수로 둡니다. 빈 문자열이면 None을 넘겨 Whisper의 기본 동작(프롬프트 없음)을 그대로 받게 합니다. 이 분기가 중요한 이유는, Whisper는 빈 문자열을 받으면 그것을 "빈 prompt"라는 신호로 잘못 해석해 디코딩이 어색해지기 때문입니다. 명시적으로 None을 넘기는 것이 안전합니다.

    # ~/.config/systemd/user/whisper-daemon.service 일부
    Environment=WHISPER_MODEL=turbo
    Environment=WHISPER_LANGUAGE=ko
    Environment="WHISPER_INITIAL_PROMPT=대화 상대의 이름·가족·자주 쓰는 어휘를 한 줄로."
    

    코드 설명 — systemd 유닛에서 환경변수를 주입합니다. Environment=의 값에 공백이 들어가면 systemd가 단어 단위로 잘라버리므로, 한국어 문장 같은 공백 포함 값은 따옴표로 감싸야 합니다. 이걸 모르고 처음 적용했을 때, 데몬은 정상 기동했는데 프롬프트가 첫 단어만 적용되는 버그가 있었습니다.

    주의 — prompt가 잘못된 방향으로 편향시키는 경우

    실제 적용해보면 두 가지 주의점이 있습니다.

    첫째, 스타일이 prompt와 어긋나면 결과가 어색해집니다. 예를 들어 prompt를 정중체 문장("안녕하십니까. 오늘 날씨가 좋습니다.")으로 두면, 아이의 반말 발화도 "~합니다" 식으로 변환돼 들어옵니다. 그래서 저는 prompt를 단순한 어휘 나열로 두고 문체는 비웠습니다. 단어 사이를 마침표·쉼표 정도만 사용해 짧게 끊습니다.

    둘째, prompt 안의 어휘가 음향과 무관한 발화를 끌어당기는 경우가 있습니다. 예를 들어 "마인크래프트"가 prompt에 들어 있는 상태에서 아이가 "맘에 들어"라고 말하면 "마인크래프트"로 빗나가곤 합니다. prompt 어휘는 신중히 골라야 하고, 너무 흔하게 끌려나오는 단어가 있다면 빼는 게 낫습니다.

    마치며 — 모델·디코더·prompt 세 개의 다이얼

    음성 인식 품질을 올릴 때 만질 수 있는 다이얼은 셋입니다. 모델 크기(small→turbo), 디코딩 옵션(beam_size 1→5), 그리고 이 글의 prompt 편향(initial_prompt). 셋은 서로 보완적입니다. 모델이 작으면 prompt도 빔도 효과가 제한적이고, 모델이 충분히 크고 빔도 살아 있는 상태에서 prompt가 화룡점정으로 작동합니다.

    비용 측면에서 prompt는 거의 공짜입니다. 디코더 prefix에 토큰 수십 개가 추가될 뿐이라 추론 시간은 거의 변하지 않습니다. 그래서 어린이 STT처럼 도메인 어휘가 좁고 또렷한 환경이라면 가장 먼저 시도해볼 만한 레버입니다.

    제 챗봇은 이 세 다이얼을 모두 돌린 뒤로 "잘 못 들었어요. 다시 한번!" 메시지를 거의 듣지 않게 됐습니다. 그 뒤에 남은 한 가지 — STT가 가끔 만들어내는 환각("구독·좋아요 부탁드립니다" 같은) — 은 모델 단에서 푸는 문제가 아니라 따로 클라이언트 측 휴리스틱 필터로 처리합니다. 다음 글에서 그 이야기로 이어갑니다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.