Qwen3-TTS 내 목소리 학습: 맥미니에서 미세조정한 과정과 결과
Qwen3-TTS로 맥미니에서 내 목소리를 합성한 실험입니다. 먼저 실제 녹음과 합성 음성을 비교하고, 짧은 참조 녹음으로 첫 문장을 만드는 절차와 미세조정 중 겪은 오류를 설명합니다.

목차
- 실제 녹음과 미세조정 결과를 먼저 듣기
- 첫 연습: 두 음성에서 문장 끝 비교하기
- 소리를 글자처럼 다루는 구조
- 제로샷 복제와 미세조정이 다른 점
- 에포크, 학습률, 손실: 미세조정에 나오는 용어
- 학습에 필요한 컴퓨터 사양
- 1단계: 짧은 녹음으로 제로샷 복제
- 2단계: 학습 데이터 만들기, 여기부터는 실험 기록
- 3단계: 미세조정 스크립트를 맥에서 돌리기
- 손실은 줄었는데 잡음만 나온 이유
- 어미를 흐리는 습관을 데이터에서 걸러내기
- 감정과 말투를 넣는 방법과 그 한계
- 나온 결과와 같은 조건으로 비교하는 설정
- 학습 중에 메모리와 디스크에서 막힌 것
- 녹음은 몇 분이나 있어야 하나요?
- 1.7B 모델도 맥미니에서 미세조정이 되나요?
- 미세조정한 목소리가 제로샷보다 항상 나은가요?
- Sources
- 3줄 요약
세 줄로 먼저 읽기
이번 방문에서 한 편은 바로 볼 수 있습니다.
Qwen3-TTS는 알리바바 큐원 팀이 2026년 1월 아파치 2.0 라이선스로 공개한 오픈소스 음성 합성(Text-to-Speech) 모델입니다. 공식 모델 카드에 따르면 10개 언어 500만 시간이 넘는 음성으로 학습했고, 한국어가 그 10개 언어에 들어 있으며, 녹음 3초 정도만 참조로 주면 그 목소리로 새 문장을 읽는 음성 복제를 지원합니다. 쉽게 말해 내 목소리 견본을 옆에 두고 어떤 문장이든 그 필체로 옮겨 적어 주는 프로그램입니다.
목소리를 복제하는 서비스는 여러 종류가 있습니다. 서비스에 따라 녹음 업로드, 이용 요금, 보관 조건이 다릅니다. 저는 녹음을 직접 관리하면서 합성하는 방법을 찾았습니다. 내 컴퓨터 안에서만 학습하고 합성할 수 있는지, 그게 엔비디아 그래픽카드가 없는 맥에서도 되는지, 학습이라는 것이 정확히 무엇을 하는 일인지는 막상 해 보기 전에는 알 수 없었습니다.
원리는 세 단계로 요약됩니다. 소리를 초당 12개의 시간 단위인 프레임으로 나누고 각 프레임을 여러 소리 토큰으로 표현한 뒤, 언어 모델이 다음 글자를 이어 쓰듯 다음 소리 토큰을 예측하고, 디코더가 그 토큰을 다시 파형으로 되돌립니다. 목소리 복제는 이 예측기에 어떤 사람의 필체를 알려 주는 일이고, 알려 주는 방법이 두 가지입니다. 견본 한 장을 옆에 놓고 따라 쓰게 하는 것이 제로샷 복제이고, 그 사람의 글씨를 수십 분치 베껴 써서 필체를 몸에 익히게 하는 것이 미세조정입니다.
이 글에서는 통합 메모리 24GB 맥미니 한 대로 두 방법을 모두 해 본 과정을 정리하겠습니다. 결과를 먼저 들려드리고, 에포크 같은 학습 용어와 필요한 컴퓨터 사양을 풀고, 실제 절차와 중간에 만난 공식 스크립트의 버그, 감정과 말투를 넣는 방법의 한계까지 순서대로 적습니다.
실제 녹음과 미세조정 결과를 먼저 듣기
글로만 설명하면 어느 정도 닮았는지 가늠이 되지 않아서 결과부터 둡니다. 첫 번째는 학습에 쓴 미팅 녹음 가운데 한 토막이고, 음량을 맞춘 것 말고는 가공하지 않은 제 실제 목소리입니다. 두 번째는 미세조정한 모델이 참조 음성 없이 화자 이름만 받아 합성한 문장이고, 학습 데이터에 없던 문장입니다.
실제 녹음 (7초): "최대한 간결하게 해서 한번 진행을 해 보겠습니다. 화면 공유를 먼저 해 드릴게요."
미세조정 모델 합성 (8.6초): "안녕하세요, 준이아빠블로그를 운영하는 홍승협입니다. 오늘은 클로드 코드로 GA4 리포트를 자동화하는 방법을 정리해 봤습니다."
실제 녹음은 대화 중이라 말이 빠르고 합성은 읽는 말투에 가까워서 속도가 다르게 들립니다. 음색은 두 개를 이어서 들어 보시면 판단이 되고, 속도와 억양의 차이가 어디서 오는지는 뒤의 감정 표현 절에서 다룹니다. 여기까지 오는 데 걸린 것은 제로샷 30분과 미세조정 세 차례였고, 그 가운데 한 차례는 실패였습니다.
첫 연습: 두 음성에서 문장 끝 비교하기
설치 없이 위 재생 버튼 두 개로 시작할 수 있습니다. 메모 앱을 열고 실제 녹음 / 합성 음성 / 확인할 부분을 적습니다. 각 음성을 한 번씩 재생하며 위에 적힌 문장과 들리는 낱말을 대조하고, 마지막 낱말까지 들리는지 기록합니다. 목소리가 닮았다는 인상과 내용이 정확하다는 판단은 따로 적습니다.
예를 들어 합성 음성: 홍승협이라는 이름이 정확히 들리는가, 마지막 말이 잘리지 않는가를 확인합니다. 두 음성의 문장이 다르므로 같은 대본의 품질 비교 실험은 아닙니다. 메모를 저장하고 다시 열어 차이 한 가지를 설명할 수 있으면 첫 연습은 끝입니다. 한 번 더 들을 때는 속도만 비교해 기록을 추가합니다. 재생이 안 되면 브라우저 음소거와 기기 볼륨부터 확인합니다.
직접 만들고 싶으면 아래 1단계까지 진행합니다. 본인 목소리나 사용에 동의받은 녹음만 준비합니다. 미세조정은 선택 사항이며, 이 글의 2단계 이후는 작성자의 실험 기록입니다. 별도 수정 스크립트 전체를 제공하는 설치 안내는 아닙니다.
소리를 글자처럼 다루는 구조
Qwen3-TTS가 소리를 처리하는 구조를 먼저 살펴보면 뒤의 학습 용어와 메모리 사용량을 이해하기 쉽습니다. 아래는 공식 저장소의 구조도입니다.
그림 아래쪽의 Qwen3 LM은 글을 쓰는 언어 모델과 같은 종류입니다. 다만 입력이 두 줄인데, 한 줄은 읽을 문장의 텍스트 토큰이고 다른 한 줄은 소리를 잘게 나눈 코덱 토큰입니다. 모델 이름의 12Hz는 초당 12프레임이라는 뜻입니다. 10초는 약 120프레임이고, 프레임마다 기본 코드 하나와 세부 코드 15개가 붙으므로 전체 소리를 토큰 120개만으로 표현한다는 뜻은 아닙니다. 언어 모델이 다음 단어를 고르듯 다음 코덱 토큰을 하나씩 고르고, 오른쪽 위 MTP 모듈이 그 토큰 하나에 세부 음질을 담당하는 토큰 15개를 덧붙이며, 마지막에 디코더가 이것을 파형으로 되돌립니다.
목소리는 어디에 들어가는지가 핵심입니다. 입력 줄 앞부분에 화자 임베딩(speaker embedding)이라는 칸이 하나 있고, 참조 음성에서 뽑은 목소리 특징을 숫자 묶음으로 만들어 그 칸에 넣습니다. 제로샷 복제는 합성할 때마다 이 칸을 참조 음성으로 채우는 방식이고, 미세조정은 학습 데이터 전체로 모델의 가중치 자체를 바꾼 뒤 그 칸에 들어갈 값을 화자 이름과 함께 모델 안에 저장해 두는 방식입니다.
공식 저장소에는 모델이 다섯 개 올라와 있고 이름 끝에 붙은 단어로 용도가 나뉩니다. 내 목소리를 쓰려면 Base가 붙은 모델을 받아야 합니다.
| 모델 | 크기 | 하는 일 | 내 목소리 복제 |
|---|---|---|---|
| Qwen3-TTS-12Hz-1.7B-Base | 1.7B | 참조 음성으로 목소리 복제, 미세조정 출발점 | 됩니다 |
| Qwen3-TTS-12Hz-0.6B-Base | 0.6B | 위와 같고 더 작습니다 | 됩니다 |
| Qwen3-TTS-12Hz-1.7B-CustomVoice | 1.7B | 미리 들어 있는 화자들로 합성하고 말투를 글로 지시 | 되지 않습니다 |
| Qwen3-TTS-12Hz-0.6B-CustomVoice | 0.6B | 미리 들어 있는 화자로 합성하며 말투 지시 기능은 지원하지 않습니다 | 되지 않습니다 |
| Qwen3-TTS-12Hz-1.7B-VoiceDesign | 1.7B | 글로 목소리를 묘사해 새 목소리를 만듭니다 | 되지 않습니다 |
이 글에서는 제로샷 복제에 1.7B Base를, 미세조정에 0.6B Base를 썼습니다. B는 모델의 파라미터, 즉 학습으로 바뀌는 수치의 수를 십억 단위로 나타낸 표기입니다. 큰 모델은 대체로 메모리가 더 필요하며 실제 사용량은 실행 방식에도 달려 있습니다.
제로샷 복제와 미세조정이 다른 점
두 방법은 필요한 재료와 결과의 성격이 다릅니다. 견본 한 장을 옆에 놓고 따라 쓰는 것과 필체 자체를 몸에 익히는 것이 다르듯, 제로샷 복제는 합성할 때마다 참조 음성을 보고 따라 하고 미세조정은 모델 안에 목소리를 새겨 둡니다.
| 항목 | 제로샷 복제 | 미세조정 |
|---|---|---|
| 필요한 녹음 | 이 글에서는 약 13초 한 토막과 그 전사문 | 이 글의 최종 데이터는 31.4분, 문장별 클립과 전사문 |
| 학습 | 없음 | 있음. 초기 97개 클립 실험에서 에포크당 약 95초 |
| 합성할 때 | 매번 참조 음성을 함께 넣음 | 화자 이름만 지정 |
| 결과의 성격 | 참조 토막의 말 빠르기를 어느 정도 따라가고 문장마다 목소리가 조금씩 달라질 수 있음 | 목소리가 일정하고 억양은 학습 데이터의 평균에 가까움 |
| 먼저 해 볼 쪽 | 제로샷 | 제로샷 결과가 아쉬울 때 |
공식 문서도 Base 모델을 제로샷 복제와 미세조정 두 용도의 출발점으로 안내합니다. 먼저 제로샷으로 들어 보고, 더 일관된 목소리가 필요할 때 같은 모델을 미세조정하는 순서가 자연스럽습니다.
에포크, 학습률, 손실: 미세조정에 나오는 용어
미세조정 명령에는 낯선 숫자가 여럿 붙습니다. 학습 과정을 연습장 베껴 쓰기에 비추면 각 숫자가 무엇을 정하는지 잡힙니다.
| 용어 | 뜻 | 이 글에서 쓴 값 |
|---|---|---|
| 에포크(epoch) | 학습 데이터 전체를 처음부터 끝까지 한 번 다 보는 단위. 연습장 한 권을 한 번 다 베껴 쓰면 1 에포크 | 6. 에포크마다 217개 클립을 한 번씩 봄 |
| 배치(batch)와 누적 | 한 번에 같이 보는 클립 수와, 몇 번을 모아서 가중치를 한 번 고칠지 | 배치 2, 누적 4. 클립 8개를 보고 한 번 고침 |
| 학습률(learning rate) | 한 번 고칠 때 가중치를 얼마나 크게 바꿀지 | 5e-6은 0.000005이며 값이 너무 크면 학습이 불안정해질 수 있음 |
| 손실(loss) | 모델이 다음 소리 토큰을 얼마나 틀리는지 나타내는 점수. 낮을수록 좋음 | 4.5에서 시작해 3.6으로 내려감 |
| 체크포인트(checkpoint) | 에포크가 끝날 때 저장하는 모델 사본 | 2.3GB짜리 2개 |
| 화자 임베딩 | 목소리 특징을 숫자 묶음으로 만든 것. 참조 음성에서 뽑음 | 13초 녹음 하나에서 뽑아 전 샘플에 같은 값 사용 |
에포크는 많을수록 좋은 것이 아닙니다. 이 글에서는 4 에포크 판이 6 에포크 판보다 자연스러웠고, 6 에포크 판은 말이 조금 느려지고 문장이 길어졌습니다. 연습장을 여러 번 베끼면 글씨는 견본을 닮아 가지만 어느 순간부터는 견본에 있던 버릇까지 과하게 따라 할 수 있습니다. 손실도 마찬가지로 낮아지는 것만으로는 안심할 수 없는데, 그 이유는 뒤의 실패 사례에서 숫자로 보여드리겠습니다.
학습에 필요한 컴퓨터 사양
공식 예제는 엔비디아 GPU를 쓰는 CUDA 설정을 보여줍니다. 제 실험에서는 애플 실리콘의 GPU를 사용하는 PyTorch MPS 백엔드로 바꾸어 제로샷과 0.6B 미세조정을 실행했습니다. 아래 값은 M4 Pro 맥미니, 통합 메모리 24GB에서 관찰한 결과이며 최소 사양이나 모든 맥의 지원 보장이 아닙니다.
| 하려는 일 | 이 글에서 확인한 것 | 해석할 범위 |
|---|---|---|
| 1.7B 제로샷 복제 | 모델 로드 68초, 15초 음성 생성 14초 | 당시 환경의 실행 시간이며 처음에는 모델 다운로드도 필요합니다 |
| 0.6B 미세조정 | 학습 프로세스가 19GB에서 23GB까지 커짐 | 초기 97개 클립에서 에포크당 약 95초였으며 217개 결과의 보장 시간이 아닙니다 |
| 1.7B 미세조정 | 시도하지 않음 | 24GB와 48GB 모두 이 글에서 동작 여부를 확인하지 않았습니다 |
| 디스크 | 모델 약 2.3GB와 4GB, 체크포인트 하나 약 2.3GB, 스왑 최대 약 40GB | 스왑은 메모리가 부족할 때 디스크에 임시로 저장하는 공간이며 30GB 여유만으로 충분하다고 할 수 없습니다 |
| 윈도우와 리눅스 | 확인하지 않음 | 공식 예제와 해당 GPU의 지원 조건을 따로 확인해야 합니다 |
메모리가 이렇게 많이 드는 이유는 미세조정이 모델의 가중치 전체를 고치는 방식이기 때문입니다. 가중치 외에도 수정 방향을 계산하는 기울기, 이전 계산을 보관하는 옵티마이저 상태, 중간 계산 결과가 메모리를 사용합니다. 길이가 다른 음성 데이터를 처리하면서 메모리 사용량이 늘어날 수도 있으며 정확한 배수는 계산 형식과 설정에 따라 다릅니다. 이 글의 0.6B 학습은 5GB 정도로 시작해 23GB까지 자랐습니다.
이 글에서 쓴 환경은 아래와 같습니다.
- 컴퓨터: M4 Pro 맥미니, 통합 메모리 24GB. 같은 용량의 맥에서 Qwen3.8 27B를 돌렸을 때는 CPU로 떨어졌는데, 이번 제로샷 모델은 더 작아 실행할 수 있었습니다. 미세조정은 메모리 부담이 컸습니다.
- 파이썬: 3.12. 공식 문서가 권하는 버전이고 uv로 가상 환경을 만들었습니다.
- 녹음 파일: 이전에 다른 용도로 잘라 둔 45초에서 60초짜리 WAV 20개(17.7분)와 미팅 녹음 51분 한 개. 미팅 녹음에는 상대방 목소리가 섞여 있어서 뒤에서 화자를 가려내는 단계가 필요했습니다.
- 전사 도구: 위스퍼(Whisper)의 맥용 판인 mlx-whisper. 참조 음성의 전사문과 학습 클립의 문장을 여기서 얻습니다.
직접 실행하려면 macOS 터미널에서 명령을 입력하고 일반 텍스트 파일을 저장할 수 있어야 합니다. 아래는 uv라는 파이썬 환경 관리 도구와 음성 변환 도구 ffmpeg가 설치되어 있다는 전제입니다. 각각 uv --version, ffmpeg -version을 입력해 버전이 나오는지 확인합니다. command not found가 나오면 uv 설치 안내와 FFmpeg 다운로드 안내를 먼저 확인하고 이 단계에서 멈춥니다.
새 터미널에서 다음 명령을 한 줄씩 실행합니다. mkdir에서 이미 존재한다는 오류가 나면 새 폴더 이름으로 바꾸어 기존 파일을 보존합니다. cd는 그 폴더로 이동하는 명령입니다.
mkdir "$HOME/qwen3-tts-practice"
cd "$HOME/qwen3-tts-practice"
uv venv --python 3.12 .venv
source .venv/bin/activate
uv pip install qwen-tts torch torchaudio soundfile
python -c "import torch; print(torch.backends.mps.is_available())"마지막 줄의 True는 MPS 사용 가능 여부입니다. 모든 모델 연산이나 메모리 용량까지 보장하지는 않습니다. False면 아래 실행을 중단하고 macOS와 PyTorch의 MPS 지원 조건을 확인합니다. 맥용 예제에서는 flash-attn을 설치하지 않습니다. 패키지와 모델을 처음 받을 때 인터넷과 디스크 공간이 필요하며, 이 코드는 로컬 녹음 경로를 모델에 전달합니다.
1단계: 짧은 녹음으로 제로샷 복제
Finder에서 홈 > qwen3-tts-practice 폴더를 열고 본인만 말하는 WAV 녹음을 original.wav라는 이름으로 복사합니다. 아래 명령은 13.18초 지점부터 13초를 잘라 24kHz 모노, 즉 한 채널의 WAV로 저장합니다. 그 구간이 없는 파일이면 시작 시각과 길이를 실제 녹음에 맞게 바꿉니다.
ffmpeg -n -ss 13.18 -i original.wav -t 13 -ar 24000 -ac 1 ref.wav
open ref.wav-n은 같은 이름의 파일을 덮어쓰지 않는 옵션입니다. 잘린 음성에 문장이 도중에 끊기거나 다른 사람이 섞이면 시작 시각을 고쳐 새 이름으로 저장합니다. 이 경우 아래 코드의 ref.wav도 새 이름으로 바꾸고 전사문을 다시 맞춥니다. 저는 약 13초를 썼지만 정확한 길이보다 혼자 말한 문장과 정확한 전사문이 중요합니다.
텍스트 편집기를 열어 포맷 > 일반 텍스트 만들기로 바꾸고 들리는 말을 그대로 적습니다. 이 파일을 실습 폴더의 ref.txt로 UTF-8 저장합니다. 자동 전사 도구는 선택 사항이며 짧은 녹음은 직접 받아 적어도 됩니다. Finder에서 파일이 ref.txt.rtf가 아닌지 확인합니다.
같은 방식으로 아래 코드를 clone.py에 저장하고 Finder에서 clone.py.txt로 저장되지 않았는지 확인합니다. 예제는 작성자가 쓴 MPS와 bfloat16 설정을 유지합니다. bfloat16은 모델 계산에 쓰는 수치 형식으로, 지원 오류가 나면 설정을 무작정 바꾸어 학습을 시작하지 말고 아래 오류 안내를 따릅니다.
from pathlib import Path
import torch
import soundfile as sf
from qwen_tts import Qwen3TTSModel
work = Path(__file__).resolve().parent
ref_text = (work / "ref.txt").read_text(encoding="utf-8").strip()
if not ref_text:
raise ValueError("ref.txt must contain the recording transcript")
output = work / "out.wav"
if output.exists():
raise FileExistsError("Rename out.wav before creating another sample")
model = Qwen3TTSModel.from_pretrained(
"Qwen/Qwen3-TTS-12Hz-1.7B-Base",
device_map="mps", dtype=torch.bfloat16, attn_implementation="sdpa",
)
prompt = model.create_voice_clone_prompt(
ref_audio=str(work / "ref.wav"), ref_text=ref_text,
)
wavs, sr = model.generate_voice_clone(
text=["안녕하세요. 오늘 회의는 오후 세 시에 시작합니다."],
language=["Korean"], voice_clone_prompt=prompt, max_new_tokens=256,
)
sf.write(str(output), wavs[0], sr)
print(output)터미널에서 다음을 실행합니다. 모델을 처음 내려받는 시간은 별도이며 제 실험의 준비 시간 30분이 누구에게나 적용되지는 않습니다.
python clone.py
open out.wav실습 폴더에 out.wav가 생기고, 재생했을 때 오후 세 시와 마지막 시작합니다까지 정확히 들리는지 확인합니다. 자동 전사와 원문 비교는 보조 수단이며 목소리와 어미는 직접 들어야 합니다. 파일을 닫았다가 Finder에서 다시 열어도 재생되면 저장까지 확인한 것입니다.
혼자 반복할 때는 기존 결과를 out-first.wav로 이름을 바꾼 뒤 코드의 세 시만 네 시로 바꾸고 다시 실행합니다. 두 파일에서 시각만 의도대로 바뀌었는지 대조합니다. 제 경우 제로샷의 음색은 가까웠지만 문장마다 변화가 있어 추가 학습을 시험했습니다. 이 정도 결과로 목적을 달성했다면 여기서 끝내도 됩니다.
| 막힌 상황 | 다음 행동 |
|---|---|
No such file 또는 전사문 오류 | Finder에서 original.wav, ref.wav, ref.txt, clone.py의 이름과 확장자를 확인합니다 |
| 패키지를 찾지 못함 | 실습 폴더에서 source .venv/bin/activate를 실행한 뒤 다시 시도합니다 |
out.wav가 이미 있음 | 기존 결과를 다른 이름으로 옮긴 뒤 다시 실행합니다 |
| MPS, dtype, 메모리 오류 | Control+C로 멈추고 오류 끝부분과 macOS, Python, PyTorch 버전을 기록합니다 |
| 생성이 길어지거나 잡음이 반복됨 | Control+C로 중단하고 짧은 문장과 참조 전사문부터 대조합니다 |
메모리 오류가 나면 작업을 저장한 뒤 큰 프로그램을 닫고 활동 모니터의 메모리 압력을 확인합니다. 같은 오류가 반복되면 학습 단계로 넘어가지 말고 환경 지원부터 확인합니다. 도움을 요청할 때는 오류 문구와 버전을 전달하고 개인 녹음 전체를 첨부할 필요는 없습니다.
2단계: 학습 데이터 만들기, 여기부터는 실험 기록
미세조정에 넣는 데이터는 문장 단위 WAV 클립과 그 전사문, 그리고 화자 참조 음성 한 개입니다. 공식 문서는 세 가지를 JSONL 한 줄에 담고, 화자 참조 음성은 모든 샘플에 같은 파일을 쓰라고 강하게 권합니다. 1단계의 13초 파일을 그대로 참조로 썼습니다.
{"audio": "clips/001.wav", "text": "이 문장의 전사문입니다.", "ref_audio": "ref.wav"}클립은 45초짜리 원본을 그대로 넣지 않고 위스퍼의 문장 타임스탬프로 4초에서 15초 사이로 다시 잘랐습니다. 처음 만든 데이터는 20개 원본에서 나온 클립 97개였습니다. 이 과정에서 두 가지를 걸러 냈습니다.
- 길이가 0인 클립: 위스퍼가 파일 끝을 넘긴 시각을 찍는 일이 있어서 잘라 보면 빈 파일이 나옵니다. 길이를 확인해 4초 미만은 뺍니다.
- 환각 문장: 무음 구간에서 위스퍼가 같은 문장을 반복해 만들어 냅니다. 같은 문장이 세 번 이상 나오면 전부 제외했습니다. 미팅 녹음에서는 이런 문장이 150건이었습니다.
미팅 녹음은 상대방 목소리가 섞여 있어서 화자를 가려내야 했는데, 별도 도구를 설치하지 않고 Qwen3-TTS 안에 들어 있는 화자 인코더를 그대로 썼습니다. 문장마다 화자 임베딩을 뽑아 참조 음성의 임베딩과 코사인 유사도, 즉 두 특징 벡터의 방향이 얼마나 가까운지를 계산하면 값이 두 무리로 나뉩니다. 제 목소리는 0.94에서 0.97, 상대방은 0.88에서 0.92에 모였고 그 사이 0.935를 경계로 잘랐습니다. 이렇게 51분 녹음에서 제 발화 29.7분을 얻었습니다. 0.935는 제 녹음에서 정한 경계이며 다른 사람과 녹음 환경에 그대로 쓰는 기준이 아닙니다. 선별한 클립도 직접 들어 다른 화자가 섞이지 않았는지 확인해야 합니다.
JSONL은 위 JSON 객체를 클립마다 한 줄씩 적은 텍스트 파일입니다. 아래는 작성자의 실행 기록이며 prepare_data.py는 패키지 설치만으로 실습 폴더에 생기지 않습니다. 공식 저장소의 finetuning 폴더와 실제 train_raw.jsonl이 필요하고, 맥에서는 코드 버전별 동작을 확인해야 합니다. 이 실험에서는 --device mps로 오디오를 코덱 토큰으로 바꾸었습니다.
python prepare_data.py --device mps \
--input_jsonl train_raw.jsonl --output_jsonl train_with_codes.jsonl3단계: 미세조정 스크립트를 맥에서 돌리기
공식 저장소의 sft_12hz.py를 바탕으로 맥용 수정 파일을 만들어 실행했습니다. 이 글에 나오는 sft_12hz_mac.py는 공식 배포 파일이 아니며 전체 수정본을 첨부하지 않았습니다. 아래 명령은 실험 설정 기록으로, 그대로 복사하면 파일이 없다는 오류가 납니다. --grad_accum도 이 수정본의 옵션입니다.
당시 0.6B 실행에서는 텍스트와 코덱 임베딩의 차원을 맞추는 text_projection 층 누락으로 오류가 났습니다. 학습에 쓰는 벡터의 길이를 맞추는 단계가 빠진 것입니다. 이 문제와 뒤의 손실 정렬 문제는 구분해야 하며, 현재 코드에 같은 수정이 필요한지는 공식 저장소의 변경 이력을 확인해야 합니다.
python sft_12hz_mac.py \
--init_model_path models/Qwen3-TTS-12Hz-0.6B-Base \
--train_jsonl train_with_codes.jsonl \
--batch_size 2 --grad_accum 4 --lr 5e-6 --num_epochs 6 \
--speaker_name hongsh학습 자체는 순조로웠습니다. 클립 97개 기준으로 에포크 하나에 약 95초가 걸렸고 손실은 12.2에서 3.6까지 내려갔습니다. 여섯 에포크가 끝나고 체크포인트로 합성을 돌렸는데, 결과는 말소리가 아니라 잡음이었고 문장이 끝나도 멈추지 않았습니다.
손실은 줄었는데 잡음만 나온 이유
학습이 가중치를 망가뜨린 것인지 추론 경로가 잘못된 것인지부터 구분해야 했습니다. 학습된 가중치를 제로샷 복제 모드로 강제로 열어 참조 음성을 주고 합성해 보니 그래도 잡음이었고, 원본 0.6B 모델은 같은 조건에서 정확한 문장을 냈습니다. 가중치 자체가 망가진 상태였습니다.
제 실험에서는 사용한 학습 코드의 손실 정렬 문제를 원인으로 판단했습니다. 저장소에도 관련 수정 요청과 정렬 문제 보고가 있습니다. 해당 보고는 사용 버전과 조건에 관한 것으로 모든 최신 설치가 같은 상태라는 뜻은 아닙니다. 스크립트가 입력과 라벨을 한 칸 미리 밀어 넣는데 트랜스포머스(Transformers) 라이브러리의 손실 함수가 안에서 또 한 칸을 밀어서, 모델이 다음 토큰이 아니라 다다음 토큰을 배우게 됩니다. 견본에서 다음 글자 대신 다다음 글자를 배우면 실제로 이어 쓸 때 순서가 어긋납니다. 앞의 구조도에서 본 MTP 모듈, 학습 코드에서는 서브 토커(sub-talker)라고 부르는 보조 예측기에도 정답이 입력으로 새어 들어가는 문제가 하나 더 있었습니다.
점검 방법 하나는 원본 모델에 같은 학습 배치를 넣고 기존 목적함수와 수정한 목적함수의 손실을 비교하는 것입니다. 2048개 후보를 같은 확률로 예측하는 단일 분류의 교차 엔트로피는 ln(2048), 약 7.62입니다. 실제 손실에 여러 항이 합쳐지거나 특별 토큰이 포함되면 이 값을 그대로 합격선으로 쓸 수 없습니다.
| 목적함수 | 원본 모델의 손실 | 판정 |
|---|---|---|
| 당시 사용 코드 | 10.17 | 수정 전 관측값 |
| 수정본 | 4.33 | 같은 배치의 수정 후 관측값 |
이 표만으로 버그가 확정되지는 않습니다. 데이터와 정답 정렬, 손실 계산 범위를 함께 확인해야 합니다. 제 실험에서는 수정 후 첫 스텝 손실이 4.5에서 시작해 3.6으로 내려갔고, 네 에포크째 체크포인트에서 실제 문장이 나오는 것까지 확인했습니다. 학습 전후에 같은 짧은 문장을 합성하고 발음, 길이, 정상 종료를 대조하는 이유입니다.
어미를 흐리는 습관을 데이터에서 걸러내기
두 번째 학습 결과를 들어 보니 문장 끝이 기어들어 가는 습관까지 그대로 배워 있었습니다. 실제로 제가 그렇게 말하기는 하는데, 내레이션으로 쓸 음성이라면 어미가 들려야 합니다. 이 부분은 실제 말버릇과 달라도 되니 학습 데이터 쪽에서 고치기로 했습니다.
먼저 문장 끝 0.5초의 음량이 문장 중앙부보다 얼마나 낮은지 측정하는 값을 하나 만들었습니다. 이 낙폭이 6dB를 넘는 문장, 소리 신호의 평균적인 크기를 나타내는 끝부분 RMS 진폭이 중앙부의 약 절반보다 낮아지는 문장은 학습에서 뺐습니다. 또박또박 쓴 페이지만 골라 연습장에 끼우는 것과 같아서, 문장 다섯 개 가운데 하나꼴로 여기서 빠졌습니다. 남은 문장에는 가벼운 압축기(acompressor)를 걸어 끝부분을 조금 올리고, 끝에 0.4초 무음을 붙여 문장이 깔끔하게 끝나는 것도 함께 배우게 했습니다.
이렇게 만든 세 번째 데이터는 클립 217개, 31.4분이었습니다. 미팅 녹음이 더해져 늘어난 숫자이고, 어미 필터가 없었다면 클립이 더 남았겠지만 어미가 들리는 문장만 남기는 쪽을 택했습니다. 같은 세 문장을 합성해 어미 낙폭을 측정하면 차이가 수치로 보입니다.
| 문장 | 두 번째 모델 | 세 번째 모델 (4 에포크) |
|---|---|---|
| 1 | -10.1 dB | -6.9 dB |
| 2 | -10.5 dB | -4.3 dB |
| 3 | -9.5 dB | -4.3 dB |
0dB에 가까우면 끝부분과 중앙부의 RMS 진폭 차이가 작다는 뜻입니다. 두 번째 모델은 약 30~34%, 세 번째 모델은 약 45~61%였으며 이 비율이 사람이 느끼는 음량이나 발음 정확도의 비율은 아닙니다. 직접 들어 본 느낌도 세 번째 모델 쪽이 깔끔했고, 글 앞머리에서 들려드린 합성이 이 모델의 4 에포크 판입니다.
감정과 말투를 넣는 방법과 그 한계
앞머리의 두 음성을 비교하면 합성 쪽이 읽는 말투로 평평하게 들립니다. 미세조정한 모델이 학습 데이터의 평균을 배우기 때문인데, 제 데이터는 미팅에서 설명하는 대화 한 종류였습니다. 그러면 감정이나 말투를 나중에 얹을 수 있는지가 궁금해지는데, 결론부터 말하면 미세조정한 모델에는 얹을 수 없었고 이 실험에서는 데이터 구성을 바꾸는 방법을 검토했습니다.
위 공식 소개 그림의 왼쪽 아래 Control이 말투를 글로 지시하는 기능입니다. "뉴스 앵커 톤으로 읽어 달라"는 식의 지시문을 붙이는 방식인데, 공식 표에서 말투 지시 지원을 표시한 모델은 1.7B CustomVoice와 1.7B VoiceDesign입니다. 0.6B CustomVoice에는 같은 지원 표시가 없습니다. 미세조정한 제 모델에도 같은 지시문 인자가 있어서 실제로 시험했습니다. 같은 문장에 지시문 없음, 차분하게, 밝게, 슬프게 네 가지를 붙여 합성했더니 네 결과가 길이와 음높이는 물론 바이트 단위까지 완전히 같았습니다. 지시문이 무시된 것이고, 공식 미세조정 스크립트가 지시문 없는 텍스트와 오디오 쌍만 학습하기 때문으로 보입니다.
제로샷 복제는 참조 토막의 말투를 어느 정도 따라갑니다. 초당 3.2음절로 느리게 말한 토막과 7.4음절로 빠르게 말한 토막을 각각 참조로 넣고 같은 문장을 만들어 보니, 결과는 초당 5.9음절과 6.4음절이었습니다. 방향은 따라가지만 폭은 참조 사이의 차이보다 훨씬 좁습니다. 참조 하나로 말투를 크게 바꾸기는 어렵다고 보는 편이 맞습니다.
그래서 감정과 말투를 넣으려면 학습 데이터에 그 상황의 녹음을 섞는 쪽이 현실적입니다.
- 읽는 말투가 필요하면 읽은 녹음을 넣습니다. 내레이션 대본을 또박또박 읽은 녹음을 추가하고 같은 문장의 변화를 비교할 수 있습니다. 필요한 분량과 비율은 이 실험에서 검증하지 않았으므로 절반 이상이면 된다는 기준을 둘 수는 없습니다.
- 감정별로 따로 학습하는 방법도 있습니다. 공식 스크립트는 화자 하나만 지원하므로, 차분한 녹음과 밝은 녹음을 각각 다른 화자 이름으로 따로 미세조정하고 문장에 따라 모델을 바꿔 쓰는 식입니다. 모델이 그만큼 늘어나고 학습도 두 번이라 시도해 볼 만한 방법이지 검증된 것은 아닙니다.
- 문장 쪽에서 조절합니다. 긴 문장을 나누고 쉼표와 마침표를 조정하면 호흡이 달라집니다. 감정을 바꾸지는 못하지만, 긴 문장을 한 번에 합성할 때 더 평평하게 들리는 경향이 있었습니다.
나온 결과와 같은 조건으로 비교하는 설정
미세조정이 끝난 모델은 참조 음성 없이 화자 이름만 지정해 합성합니다. 합성은 표본 추출(sampling) 방식이라 같은 문장도 돌릴 때마다 조금씩 달라지는데, 제 환경에서는 시드라는 난수 시작값을 고정해 같은 입력의 결과를 반복했습니다. 다만 PyTorch 재현성 안내처럼 버전과 장비가 달라지면 완전히 같은 결과가 보장되지 않습니다. 문장이 바뀔 때 말투가 일정하다는 보장도 아닙니다.
아래는 학습이 끝난 모델의 합성 설정을 보여주는 발췌 코드입니다. 첫 실습에 그대로 붙여 넣는 코드는 아니며 해당 체크포인트와 화자 등록이 필요합니다.
import torch
from qwen_tts import Qwen3TTSModel
sentences = ["오늘 회의는 오후 세 시에 시작합니다."]
torch.manual_seed(7)
model = Qwen3TTSModel.from_pretrained("output/checkpoint-epoch-3", device_map="mps",
dtype=torch.bfloat16, attn_implementation="sdpa")
wavs, sr = model.generate_custom_voice(
text=sentences, language=["Korean"] * len(sentences), speaker="hongsh",
temperature=0.75, subtalker_temperature=0.75, max_new_tokens=600,
)실제로 쓰는 스크립트는 여기에 세 가지를 더했습니다. 마침표 기준으로 문장을 나눠 따로 합성하고, 문장마다 앞뒤 무음을 정리한 뒤 0.35초 간격으로 이어 붙이고, max_new_tokens에 상한을 둡니다. 마지막 항목은 안전장치인데, 8192프레임은 12Hz 기준 약 11.4분의 오디오에 해당합니다. 이는 생성되는 음성의 길이를 설명하는 계산이며 실행 대기 시간이 11분이라는 뜻은 아닙니다. 첫 실습은 상한 256으로 짧게 제한했습니다.
제 0.6B 실험의 생성 시간은 음성 길이의 약 절반이었습니다. 같은 속도가 유지된다고 가정하면 10분 음성에 약 5분이지만 긴 대본 전체의 보장 시간은 아닙니다. 톤은 문장마다 조금씩 달라서 어떤 문장은 어미가 다시 흐려지기도 하는데, 그 문장만 시드를 바꿔 다시 만들면 되고 대본 전체를 다시 만들 필요는 없습니다.
학습 중에 메모리와 디스크에서 막힌 것
사양 절에서 말한 대로 0.6B 학습 프로세스는 23GB까지 자랐습니다. 통합 메모리 24GB 컴퓨터에서 다른 프로그램이 함께 떠 있으면 스왑이 시작되고, 에포크 시간이 95초에서 300초, 심할 때는 28분까지 늘어났습니다.
- 학습 전에 큰 프로그램을 닫습니다. 제 경우 백그라운드에 떠 있던 프로그램들의 표시 메모리 사용량 합계가 33GB였고, 그것들을 닫자 스왑이 38GB에서 19GB로 줄고 속도가 돌아왔습니다.
- 체크포인트 하나가 2.3GB입니다. 공식 스크립트는 에포크마다 모델 폴더 전체를 복사해 저장하므로 여섯 에포크면 14GB입니다. 저장할 에포크를 골라 두고 저장 직전에 디스크 여유를 확인합니다.
- 스왑이 디스크를 차지합니다. 학습 도중 디스크 여유가 14GB에서 3GB로 줄어 있었는데 원인은 스왑 파일이었습니다. 체크포인트 저장이 실패하면 그 에포크가 통째로 날아가므로 여유를 두는 편이 안전합니다.
녹음은 몇 분이나 있어야 하나요?
공식 문서는 3초 참조 음성의 복제 기능을 소개하지만 결과 품질은 녹음에 따라 달라집니다. 실제로는 문장이 끊기지 않는 10초에서 15초 한 토막이 안정적이었습니다. 미세조정의 최종 데이터는 이 글에서 31.4분이었으며 이 시간이 최소 요구량은 아닙니다. 분량보다 중요한 것은 종류인데, 미팅 대화 한 가지로만 학습하면 억양이 그 상황의 평균으로 평평해집니다. 혼자 또박또박 읽은 내레이션이나 강의하듯 말한 녹음이 섞이면 폭이 넓어집니다.
1.7B 모델도 맥미니에서 미세조정이 되나요?
이 글에서는 1.7B 미세조정을 시험하지 않았습니다. 0.6B도 23GB까지 사용했으므로 24GB 환경에서 더 큰 모델을 학습하면 메모리 부족이 우려됩니다. 48GB라고 성공한다고 단정할 수도 없습니다. 새 장비를 사기 전에 현재 장비의 제로샷 결과로 목적을 달성하는지 먼저 판단하는 편이 좋습니다.
미세조정한 목소리가 제로샷보다 항상 나은가요?
그렇지는 않습니다. 저장소 이슈에는 미세조정 결과가 제로샷보다 단조롭게 들렸다는 보고가 여럿 있고, 제 결과도 억양 폭은 제로샷 쪽이 넓었습니다. 미세조정이 나은 점은 문장을 많이 만들어도 이 실험에서 느낀 목소리의 일관성과 참조 음성을 매번 넣지 않아도 되는 편의입니다. 짧은 문장 몇 개라면 제로샷으로 충분하고, 긴 내레이션에서 일관성이 부족할 때 미세조정의 준비 비용과 실제 개선을 비교해 결정할 수 있습니다.
Sources
- Qwen3-TTS 공식 GitHub 저장소
- Qwen3-TTS 미세조정 공식 안내
- Qwen3-TTS-12Hz-0.6B-Base 모델 카드
- Qwen3-TTS 기술 보고서
- 학습 손실 정렬 문제 보고 #371
- PyTorch 공식 문서: MPS 백엔드
3줄 요약
-
제로샷 복제는 참조 녹음으로 새 문장을 합성하고, 미세조정은 추가 녹음으로 모델 자체를 바꾸는 방법입니다.
-
처음에는 본인 녹음과 전사문으로 짧은 문장 하나를 만들고, 저장한 음성을 다시 열어 발음과 문장 끝을 대조합니다.
-
맥미니의 학습 시간과 메모리 사용량은 이 실험의 조건이며, 손실이 낮아져도 실제 합성 결과가 정확한지는 별도로 확인해야 합니다.

제대로 이해했는지 한 문제로 확인해 볼까요?
답을 고르면 바로 풀이가 나와요.
미세조정 중 손실은 줄었지만 결과가 잡음이고 멈추지 않습니다. 다음 행동으로 적절한 것은 무엇인가요?
이 글이 도움이 되었나요?
내 컴퓨터에서 AI를 돌릴 때 필요한 하드웨어, 모델 크기, 실행 도구를 순서대로
코스 전체 보기 →
새 글과 AI 소식을 메일로 받아 보세요
AI가 바꾸는 일과 도구, 측정 실무 이야기를 매주 한 번 보내 드려요.
