이미지 로딩 중...

Gemma 모델 Vocabulary 확장 전략 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 11. 19. · 4 Views

Gemma 모델 Vocabulary 확장 전략 완벽 가이드

Gemma 사전학습 모델에 SNAC 오디오 토큰을 추가하여 음성 합성 능력을 갖춘 멀티모달 모델로 확장하는 방법을 단계별로 알아봅니다. Vocabulary 분석부터 토큰 추가, 임베딩 초기화까지 실전 노하우를 공유합니다.


목차

  1. Pretrained LLM 선택 (Gemma-3-270m vs 2B)
  2. 기존 Vocabulary 분석
  3. SNAC Special Tokens 추가 (<audio_start>, <audio_end>)
  4. 12,288개 SNAC Tokens 추가 (3 layers × 4096 codes)
  5. 모델 Embedding Layer 리사이징
  6. 새로운 토큰 초기화 전략

1. Pretrained LLM 선택 (Gemma-3-270m vs 2B)

시작하며

여러분이 텍스트만 이해하는 AI 모델에 음성 기능을 추가하고 싶을 때, 가장 먼저 어떤 기본 모델을 선택해야 할지 고민되시나요? "큰 모델이 무조건 좋은 거 아닐까?"라고 생각하기 쉽지만, 실제로는 프로젝트 목적에 따라 현명한 선택이 필요합니다.

이 선택은 단순히 성능만의 문제가 아닙니다. 학습 시간, 필요한 컴퓨팅 자원, 그리고 실제 서비스 배포 시 비용까지 모든 것에 영향을 미칩니다.

잘못 선택하면 몇 주간의 학습 시간과 수백만 원의 GPU 비용을 낭비할 수 있죠. 바로 이럴 때 필요한 것이 체계적인 사전학습 모델 선택 전략입니다.

Gemma-3-270m과 2B 모델의 차이를 이해하고, 여러분의 프로젝트에 맞는 선택을 할 수 있게 도와드리겠습니다.

개요

간단히 말해서, Gemma 모델 선택은 여러분의 리소스와 목표 사이의 균형점을 찾는 것입니다. 270m(2억 7천만 파라미터)은 빠른 실험용, 2B(20억 파라미터)는 본격적인 성능 추구용이라고 생각하시면 됩니다.

왜 이 선택이 중요한가요? TTS(Text-to-Speech) 모델을 만들 때는 기존 언어 모델에 12,000개 이상의 오디오 토큰을 추가해야 합니다.

예를 들어, 빠른 프로토타이핑을 원한다면 270m 모델로 시작해서 몇 시간 만에 첫 결과를 볼 수 있고, 실제 서비스급 품질이 필요하다면 2B 모델로 며칠간 학습해야 합니다. 기존에는 처음부터 큰 모델로 시작해서 시간과 비용을 낭비했다면, 이제는 270m으로 빠르게 검증하고 2B로 스케일업하는 전략을 사용할 수 있습니다.

270m 모델의 핵심 특징은 빠른 iteration, 낮은 메모리 사용량(16GB GPU에서도 가능), 그리고 개념 검증용으로 완벽하다는 점입니다. 반면 2B는 더 나은 언어 이해력, 자연스러운 음성 생성, 그리고 프로덕션 품질을 제공합니다.

이러한 특징들이 프로젝트의 성공과 효율성을 결정짓습니다.

코드 예제

from transformers import AutoModelForCausalLM, AutoTokenizer

# 빠른 실험용: Gemma-3-270m 선택
# 주석: 메모리 효율적이고 빠른 iteration에 적합
model_name = "google/gemma-3-270m"

# 프로덕션용: Gemma-2B 선택 (주석 해제하여 사용)
# model_name = "google/gemma-2b"

# 주석: 모델과 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype="auto",  # 자동으로 최적 dtype 선택
    device_map="auto"    # GPU 메모리에 자동 배치
)

# 주석: 현재 모델 정보 확인
print(f"모델 파라미터 수: {model.num_parameters():,}")
print(f"Vocabulary 크기: {len(tokenizer)}")

설명

이것이 하는 일: 위 코드는 Hugging Face 라이브러리를 사용해서 Gemma 사전학습 모델을 로드하고, 현재 모델의 기본 정보를 확인합니다. 이는 Vocabulary 확장 작업의 첫 번째 단계입니다.

첫 번째로, model_name 변수로 어떤 모델을 사용할지 선택합니다. 주석 처리를 통해 쉽게 모델을 전환할 수 있도록 설계되어 있죠.

이렇게 하는 이유는 개발 단계에서는 270m을, 실제 학습 단계에서는 2B를 사용하는 워크플로우를 지원하기 위함입니다. 그 다음으로, AutoTokenizerAutoModelForCausalLM을 사용해서 모델을 메모리에 로드합니다.

torch_dtype="auto"는 자동으로 최적의 데이터 타입(보통 float16 또는 bfloat16)을 선택하여 메모리를 절약하고, device_map="auto"는 사용 가능한 GPU에 모델을 자동으로 배치합니다. 마지막으로, 모델의 파라미터 개수와 현재 Vocabulary 크기를 출력합니다.

270m 모델은 약 2억 7천만 개, 2B 모델은 약 20억 개의 파라미터를 가지며, 기본 Vocabulary는 보통 256,000개 정도입니다. 이 정보는 다음 단계에서 SNAC 토큰을 추가할 때 중요한 기준점이 됩니다.

여러분이 이 코드를 사용하면 몇 분 안에 모델이 로드되고, 현재 상태를 정확히 파악할 수 있습니다. 270m 모델은 16GB GPU에서도 문제없이 작동하고, 2B 모델은 24GB 이상을 권장합니다.

또한 모델 선택을 주석 하나로 전환할 수 있어 실험이 매우 편리해집니다.

실전 팁

💡 처음에는 반드시 270m 모델로 전체 파이프라인을 검증하세요. 하루 안에 end-to-end 테스트를 완료할 수 있어 버그를 조기에 발견할 수 있습니다.

💡 GPU 메모리가 부족하다면 load_in_8bit=True 옵션을 추가하세요. 성능은 약간 떨어지지만 메모리를 절반으로 줄일 수 있습니다.

💡 2B 모델은 학습 시 gradient checkpointing을 활성화하세요. 메모리 사용량을 30-40% 줄일 수 있어 더 큰 배치 사이즈로 학습 가능합니다.

💡 모델 파라미터 수와 Vocabulary 크기를 반드시 기록해두세요. 나중에 토큰 확장 후 비교할 때 필수 정보입니다.

💡 AutoModelForCausalLM 대신 AutoModel을 사용하지 마세요. TTS는 생성 작업이므로 CausalLM 헤드가 필요합니다.


2. 기존 Vocabulary 분석

시작하며

여러분이 모델에 새로운 토큰을 추가하려고 할 때, "그냥 추가하면 되는 거 아닌가?"라고 생각하기 쉽습니다. 하지만 기존 토큰들과 충돌하거나, 이미 사용 중인 특수 토큰을 덮어쓰는 실수를 범하면 모델 전체가 망가질 수 있습니다.

이런 문제는 실제로 많은 개발자들이 겪는 고통입니다. 몇 주간 학습한 모델이 이상한 출력을 내놓거나, 특정 토큰에서 오류가 발생하는 경우가 바로 Vocabulary 분석을 제대로 하지 않아서 발생합니다.

바로 이럴 때 필요한 것이 체계적인 Vocabulary 분석입니다. 현재 어떤 토큰들이 있는지, 특수 토큰은 무엇인지, 그리고 새 토큰을 안전하게 추가할 수 있는 공간이 어디인지 정확히 파악해야 합니다.

개요

간단히 말해서, Vocabulary 분석은 모델이 현재 이해하는 "단어 사전"을 조사하는 작업입니다. 사람으로 치면 뇌 속 언어 체계를 스캔하는 것과 같습니다.

왜 이 분석이 필요한가요? Gemma 모델은 이미 256,000개 정도의 토큰을 가지고 있고, 그 중 일부는 <bos>, <eos>, <pad> 같은 특수한 역할을 합니다.

예를 들어, 우리가 <audio_start> 같은 새 특수 토큰을 추가할 때, 기존 토큰과 겹치지 않는 ID를 할당해야 합니다. 기존에는 토큰 충돌을 확인하지 않고 추가해서 학습 중 에러가 발생했다면, 이제는 사전에 철저히 분석하고 안전한 ID 범위를 확보할 수 있습니다.

Vocabulary 분석의 핵심 특징은 세 가지입니다: 현재 토큰 개수 파악, 특수 토큰 목록 확인, 그리고 사용 가능한 ID 범위 계산입니다. 이러한 정보들이 없으면 토큰 추가 작업은 사실상 도박이 되어버립니다.

코드 예제

from transformers import AutoTokenizer

# 주석: 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained("google/gemma-3-270m")

# 주석: 기본 Vocabulary 정보 출력
print(f"현재 Vocabulary 크기: {len(tokenizer)}")
print(f"모델 최대 토큰 ID: {tokenizer.vocab_size - 1}")

# 주석: 특수 토큰 확인 (중요: 충돌 방지용)
special_tokens = tokenizer.all_special_tokens
special_ids = tokenizer.all_special_ids
print(f"\n특수 토큰 개수: {len(special_tokens)}")
for token, token_id in zip(special_tokens, special_ids):
    print(f"  {token}: ID {token_id}")

# 주석: 토큰 타입 분포 확인
print(f"\n패딩 토큰: {tokenizer.pad_token}")
print(f"시작 토큰: {tokenizer.bos_token}")
print(f"종료 토큰: {tokenizer.eos_token}")

설명

이것이 하는 일: 위 코드는 Gemma 모델의 현재 Vocabulary를 분석하여 토큰 추가 전 필수 정보를 수집합니다. 마치 건물을 증축하기 전에 도면을 확인하는 것과 같습니다.

첫 번째로, len(tokenizer)로 현재 총 토큰 개수를 확인합니다. Gemma 모델은 보통 256,000개 정도를 가지고 있는데, 이 숫자가 우리가 새로 추가할 토큰의 시작 ID가 됩니다.

예를 들어 256,000개라면, 새 토큰은 ID 256,000부터 시작해야 겹치지 않습니다. 그 다음으로, all_special_tokensall_special_ids를 사용해서 특수 토큰 목록을 추출합니다.

이 토큰들은 절대 삭제하거나 덮어쓰면 안 되는 중요한 제어 토큰들입니다. <bos>는 문장 시작, <eos>는 문장 끝, <pad>는 배치 처리 시 길이 맞추기에 사용됩니다.

마지막으로, 각 특수 토큰의 역할을 출력합니다. 이 정보는 나중에 <audio_start>, <audio_end> 같은 오디오 특수 토큰을 추가할 때, 비슷한 역할을 하는 토큰을 참고하는 데 유용합니다.

여러분이 이 분석을 수행하면 토큰 ID 충돌을 100% 방지할 수 있고, 특수 토큰을 실수로 손상시키는 것을 막을 수 있습니다. 또한 Vocabulary 확장 전후를 명확히 비교할 수 있는 기준점이 생깁니다.

실무에서는 이 정보를 JSON 파일로 저장해서 팀원들과 공유하는 것이 좋습니다.

실전 팁

💡 tokenizer.get_vocab()을 사용하면 모든 토큰과 ID의 딕셔너리를 얻을 수 있습니다. 토큰 충돌 디버깅 시 매우 유용합니다.

💡 특수 토큰 ID는 보통 0-100 범위에 있습니다. 새 특수 토큰을 추가할 때는 기존 범위를 피하고 Vocabulary 끝부분에 추가하세요.

💡 tokenizer.convert_ids_to_tokens([0, 1, 2])로 특정 ID의 토큰을 확인할 수 있습니다. 의심스러운 ID가 있을 때 빠르게 검증하세요.

💡 Vocabulary 분석 결과를 Excel이나 CSV로 저장해두세요. 나중에 토큰 사용 패턴을 분석하거나 문제를 추적할 때 필수입니다.

💡 tokenizer.is_fast 속성을 확인하세요. Fast tokenizer는 Rust로 구현되어 있어 분석 속도가 10배 이상 빠릅니다.


3. SNAC Special Tokens 추가 (<audio_start>, <audio_end>)

시작하며

여러분이 텍스트와 오디오를 섞어서 처리하는 모델을 만들 때, 모델이 "여기서부터 오디오야"라고 어떻게 알 수 있을까요? 그냥 오디오 토큰을 넣으면 텍스트와 구분이 안 되어서 모델이 혼란스러워합니다.

이 문제는 실제로 멀티모달 모델 개발에서 가장 흔한 실수입니다. 경계 표시 없이 데이터를 섞으면 모델이 오디오를 텍스트처럼 해석하거나, 반대로 텍스트를 오디오처럼 처리해서 완전히 엉뚱한 결과를 만들어냅니다.

바로 이럴 때 필요한 것이 특수 경계 토큰입니다. <audio_start><audio_end>를 추가하면 모델이 명확하게 오디오 구간을 인식하고, 텍스트와 다르게 처리할 수 있습니다.

개요

간단히 말해서, 특수 경계 토큰은 모델에게 "이제부터 다른 종류의 데이터가 시작됩니다"라고 알려주는 신호등 같은 역할입니다. HTML의 <div></div> 태그처럼 구간을 명확히 구분해줍니다.

왜 이 토큰들이 필요한가요? SNAC 코덱은 오디오를 12,288개의 숫자 토큰으로 변환하는데, 이 토큰들이 일반 텍스트 토큰과 섞이면 모델이 혼란스러워합니다.

예를 들어, "안녕하세요 <audio_start> [오디오 토큰들] <audio_end> 반갑습니다" 같은 형식으로 데이터를 구성할 때 경계 토큰이 필수입니다. 기존에는 경계 없이 데이터를 연결해서 학습이 불안정했다면, 이제는 명확한 구분자로 모달리티를 분리하여 안정적인 학습이 가능합니다.

특수 토큰 추가의 핵심은 세 가지입니다: 고유한 토큰 이름 정의, 기존 Vocabulary에 안전하게 추가, 그리고 토크나이저 설정 업데이트입니다. 이 단계를 정확히 거치면 모델이 텍스트와 오디오를 완벽히 구분할 수 있습니다.

코드 예제

from transformers import AutoTokenizer

# 주석: 기존 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained("google/gemma-3-270m")
original_vocab_size = len(tokenizer)

# 주석: SNAC 특수 토큰 정의
special_tokens_dict = {
    "additional_special_tokens": [
        "<audio_start>",  # 오디오 시작 마커
        "<audio_end>"     # 오디오 종료 마커
    ]
}

# 주석: 토크나이저에 특수 토큰 추가
num_added = tokenizer.add_special_tokens(special_tokens_dict)
print(f"추가된 특수 토큰 개수: {num_added}")

# 주석: 추가된 토큰 ID 확인 (중요: 모델 리사이징에 필요)
audio_start_id = tokenizer.convert_tokens_to_ids("<audio_start>")
audio_end_id = tokenizer.convert_tokens_to_ids("<audio_end>")
print(f"<audio_start> ID: {audio_start_id}")
print(f"<audio_end> ID: {audio_end_id}")
print(f"새 Vocabulary 크기: {len(tokenizer)}")

설명

이것이 하는 일: 위 코드는 SNAC 오디오 데이터의 경계를 표시하는 특수 토큰 2개를 토크나이저에 추가합니다. 마치 책에 장(chapter) 구분을 표시하는 것과 같습니다.

첫 번째로, 원본 Vocabulary 크기를 저장해둡니다. 이는 나중에 몇 개의 토큰이 추가되었는지 확인하는 용도입니다.

그 다음 additional_special_tokens 키를 사용해서 새로운 특수 토큰 목록을 정의합니다. 이 방식을 사용하는 이유는 Hugging Face가 특수 토큰을 일반 토큰과 다르게 처리하도록 보장하기 때문입니다.

그 다음으로, add_special_tokens() 메서드를 호출합니다. 이 함수는 자동으로 Vocabulary 끝부분에 토큰을 추가하고, 충돌을 방지하며, 내부 매핑 테이블을 업데이트합니다.

반환값 num_added는 실제로 추가된 토큰 개수로, 2가 나와야 정상입니다. 마지막으로, convert_tokens_to_ids()로 새 토큰의 ID를 확인합니다.

보통 원본 Vocabulary 크기가 256,000이었다면, <audio_start>는 256,000, <audio_end>는 256,001의 ID를 받습니다. 이 ID는 다음 단계에서 모델의 임베딩 레이어를 확장할 때 반드시 필요합니다.

여러분이 이 코드를 실행하면 토크나이저가 즉시 업데이트되고, 새 토큰을 인식할 수 있게 됩니다. 이제 tokenizer.encode("텍스트 <audio_start> 오디오") 같은 입력이 가능해지고, 모델이 오디오 구간을 명확히 파악할 수 있습니다.

실무에서는 이 업데이트된 토크나이저를 반드시 저장해서 학습과 추론에 동일하게 사용해야 합니다.

실전 팁

💡 특수 토큰 이름에는 <>를 사용하세요. 일반 텍스트에 나타날 가능성이 거의 없어 충돌을 방지할 수 있습니다.

💡 add_special_tokens() 호출 후 반드시 tokenizer.save_pretrained()로 저장하세요. 나중에 추론할 때 동일한 토크나이저를 사용하지 않으면 토큰 ID 불일치 에러가 발생합니다.

💡 토큰 추가 후 tokenizer.encode()tokenizer.decode()로 왕복 테스트를 하세요. "<audio_start>"가 올바르게 인코딩/디코딩되는지 확인하세요.

💡 여러 개의 특수 토큰을 추가할 때는 리스트 순서가 ID 할당 순서입니다. 논리적인 순서로 정렬하세요.

💡 tokenizer.all_special_tokens로 추가 후 전체 특수 토큰 목록을 확인하세요. 기존 토큰들과 함께 새 토큰이 보여야 합니다.


4. 12,288개 SNAC Tokens 추가 (3 layers × 4096 codes)

시작하며

여러분이 오디오를 AI가 이해할 수 있는 숫자로 바꾸려고 할 때, 몇 개의 토큰이 필요할까요? 놀랍게도 SNAC 코덱은 고품질 오디오를 표현하기 위해 무려 12,288개의 토큰이 필요합니다.

"왜 이렇게 많아?"라고 생각하시겠지만, 오디오의 풍부한 정보를 담으려면 불가피합니다. 이 대량의 토큰 추가는 단순히 숫자만 늘리는 게 아닙니다.

계층 구조를 이해하고, 각 레이어의 역할을 파악하고, 토큰 ID를 체계적으로 할당해야 합니다. 무작정 추가하면 메모리 폭발이나 학습 불안정성 같은 심각한 문제가 발생합니다.

바로 이럴 때 필요한 것이 구조화된 대량 토큰 추가 전략입니다. SNAC의 3-layer 구조를 이해하고, 각 레이어별로 4,096개씩 체계적으로 토큰을 할당하는 방법을 알려드리겠습니다.

개요

간단히 말해서, SNAC은 오디오를 3개의 계층으로 나누어 표현하는 계층적 코덱입니다. 1층은 전체적인 윤곽(coarse), 2층은 중간 디테일(mid), 3층은 세밀한 디테일(fine)을 담당합니다.

왜 3개 레이어로 나누나요? 한 번에 모든 정보를 표현하면 너무 복잡해지고, 모델이 학습하기 어렵습니다.

예를 들어, 음악의 전체적인 멜로디는 1층, 악기의 질감은 2층, 미세한 떨림은 3층으로 나누면 모델이 단계적으로 학습할 수 있습니다. 각 레이어는 4,096개의 코드북을 가지므로 총 3 × 4,096 = 12,288개의 토큰이 필요합니다.

기존에는 오디오를 단일 레벨로 표현해서 품질이 떨어졌다면, 이제는 계층 구조로 고품질 음성 합성이 가능합니다. 핵심 특징은: 체계적인 ID 할당(레이어별로 구분), 명확한 네이밍 규칙(<snac_0_0>부터 <snac_2_4095>까지), 그리고 효율적인 메모리 관리입니다.

이 구조를 정확히 구현하면 모델이 오디오를 텍스트만큼 자연스럽게 생성할 수 있습니다.

코드 예제

from transformers import AutoTokenizer

# 주석: 토크나이저 로드 (특수 토큰 추가 완료 상태)
tokenizer = AutoTokenizer.from_pretrained("google/gemma-3-270m")

# 주석: SNAC 토큰 생성 (3 layers × 4096 codes)
snac_tokens = []
num_layers = 3
num_codes = 4096

for layer in range(num_layers):
    for code in range(num_codes):
        # 주석: 형식 <snac_레이어_코드> (예: <snac_0_1234>)
        token = f"<snac_{layer}_{code}>"
        snac_tokens.append(token)

print(f"생성된 SNAC 토큰 개수: {len(snac_tokens)}")

# 주석: 토크나이저에 SNAC 토큰 추가
tokenizer.add_tokens(snac_tokens)
print(f"최종 Vocabulary 크기: {len(tokenizer)}")
print(f"추가된 총 토큰 수: {len(tokenizer) - 256000}")

설명

이것이 하는 일: 위 코드는 SNAC 코덱의 모든 오디오 토큰을 생성하고 토크나이저에 추가합니다. 마치 새로운 언어의 알파벳 12,288개를 사전에 등록하는 것과 같습니다.

첫 번째로, 이중 반복문으로 모든 조합을 생성합니다. 외부 루프는 레이어(0, 1, 2)를 순회하고, 내부 루프는 각 레이어의 코드(0~4095)를 순회합니다.

f-string을 사용해서 <snac_0_0>, <snac_0_1>, ... <snac_2_4095> 형태로 토큰을 만듭니다.

이렇게 명확한 네이밍을 사용하는 이유는 나중에 디버깅할 때 어느 레이어의 어느 코드인지 즉시 알 수 있기 때문입니다. 그 다음으로, 생성된 12,288개 토큰을 리스트에 담아 add_tokens() 메서드로 한 번에 추가합니다.

여기서 주의할 점은 add_special_tokens()가 아닌 add_tokens()를 사용한다는 것입니다. SNAC 토큰은 특수한 제어 기능이 없고 단순히 오디오 데이터를 표현하는 일반 토큰이기 때문입니다.

마지막으로, 최종 Vocabulary 크기를 확인합니다. 원본 256,000 + 특수 토큰 2 + SNAC 토큰 12,288 = 268,290개가 나와야 정상입니다.

이 숫자는 다음 단계에서 모델의 임베딩 레이어 크기를 결정하는 중요한 값입니다. 여러분이 이 코드를 실행하면 몇 초 안에 12,288개 토큰이 추가되고, 토크나이저가 오디오 데이터를 인코딩할 준비가 완료됩니다.

실무에서는 토큰 추가 후 tokenizer.encode("<snac_1_2048>")로 샘플 테스트를 해서 올바르게 작동하는지 확인하세요. 메모리 사용량은 토큰 수에 비례하므로, 이 작업 후 약 50MB 정도의 추가 메모리가 필요합니다.

실전 팁

💡 토큰 이름에 레이어와 코드 번호를 명시하세요. <snac_1> 같은 애매한 이름보다 <snac_1_1234> 같이 구체적인 게 디버깅에 유리합니다.

💡 12,288개 토큰을 한 번에 추가하는 게 루프로 하나씩 추가하는 것보다 100배 이상 빠릅니다. 리스트를 먼저 만들고 한 번에 추가하세요.

💡 토큰 추가 후 len(tokenizer)가 예상 크기와 정확히 일치하는지 assert로 검증하세요. 작은 차이도 나중에 큰 문제를 일으킵니다.

💡 SNAC 토큰 순서가 중요합니다. 반드시 layer → code 순서로 생성해서 ID가 연속적이 되도록 하세요. 추론 시 효율성이 높아집니다.

💡 tokenizer.get_added_vocab()으로 새로 추가된 토큰만 따로 확인할 수 있습니다. 원본 토큰과 구분해서 관리하세요.


5. 모델 Embedding Layer 리사이징

시작하며

여러분이 토크나이저에 12,000개 이상의 토큰을 추가했다면, 모델도 이 토큰들을 이해할 수 있게 업데이트해야 합니다. 그런데 "그냥 토크나이저만 수정하면 되는 거 아닌가?"라고 생각하셨다면 큰 오산입니다.

토크나이저와 모델의 Vocabulary 크기가 불일치하면 즉시 크래시가 발생합니다. 이 문제는 초보자들이 가장 많이 겪는 에러입니다.

"RuntimeError: index out of range"라는 에러 메시지를 보고 몇 시간을 헤매는 경우가 부지기수죠. 원인은 단순합니다: 모델의 임베딩 테이블이 새 토큰 ID를 처리할 수 없기 때문입니다.

바로 이럴 때 필요한 것이 임베딩 레이어 리사이징입니다. 모델의 입력 임베딩과 출력 임베딩 레이어를 확장해서 새로운 토큰을 수용할 수 있는 공간을 만들어야 합니다.

개요

간단히 말해서, 임베딩 레이어 리사이징은 모델의 "단어장" 크기를 늘리는 작업입니다. 원래 256,000개 단어를 이해하던 모델을 268,290개 단어를 이해하도록 확장하는 것이죠.

왜 이 작업이 필요한가요? 모델의 첫 레이어(입력 임베딩)는 토큰 ID를 벡터로 변환하고, 마지막 레이어(출력 임베딩)는 벡터를 토큰 확률로 변환합니다.

예를 들어, 토큰 ID 260,000을 입력하면 임베딩 테이블이 256,000까지만 있으면 에러가 발생합니다. 268,290까지 확장해야 안전합니다.

기존에는 수동으로 PyTorch 텐서를 조작해서 에러가 잦았다면, 이제는 Hugging Face의 resize_token_embeddings() 메서드로 안전하게 리사이징할 수 있습니다. 핵심 특징은: 자동으로 가중치 복사, 새 토큰 영역 초기화, 그리고 모델 설정 업데이트입니다.

이 메서드는 기존 학습된 토큰의 임베딩은 보존하면서 새 토큰 공간만 추가하는 똑똑한 방식으로 작동합니다.

코드 예제

from transformers import AutoModelForCausalLM, AutoTokenizer

# 주석: 확장된 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained("./updated_tokenizer")
new_vocab_size = len(tokenizer)

# 주석: 원본 모델 로드
model = AutoModelForCausalLM.from_pretrained("google/gemma-3-270m")
original_vocab_size = model.config.vocab_size

print(f"원본 Vocabulary: {original_vocab_size}")
print(f"새 Vocabulary: {new_vocab_size}")

# 주석: 임베딩 레이어 리사이징 (핵심 단계!)
model.resize_token_embeddings(new_vocab_size)

# 주석: 리사이징 확인
print(f"모델 임베딩 크기: {model.get_input_embeddings().weight.shape[0]}")
print(f"출력 임베딩 크기: {model.get_output_embeddings().weight.shape[0]}")

설명

이것이 하는 일: 위 코드는 모델의 임베딩 레이어를 확장된 Vocabulary 크기에 맞춰 업데이트합니다. 마치 서랍장에 새로운 칸을 추가하는 것과 같습니다.

첫 번째로, 업데이트된 토크나이저를 로드해서 새로운 Vocabulary 크기를 확인합니다. 이 값은 268,290이어야 합니다.

동시에 원본 모델을 로드해서 현재 임베딩 크기(256,000)를 확인합니다. 이 차이를 보면 얼마나 많은 토큰이 추가되었는지 알 수 있습니다.

그 다음으로, resize_token_embeddings() 메서드를 호출합니다. 이 메서드는 내부적으로 매우 복잡한 작업을 수행합니다: 기존 임베딩 가중치를 새로운 더 큰 텐서로 복사하고, 새로 추가된 12,290개 토큰 영역을 랜덤하게 초기화하고, 모델의 config를 업데이트합니다.

이 모든 과정이 자동으로 처리되어 에러 가능성을 최소화합니다. 마지막으로, get_input_embeddings()get_output_embeddings()로 실제 임베딩 레이어의 크기를 확인합니다.

두 값 모두 268,290이 나와야 정상입니다. 입력과 출력 임베딩이 모두 동일한 크기로 확장되어야 모델이 제대로 작동합니다.

여러분이 이 작업을 완료하면 모델이 새 토큰을 에러 없이 처리할 수 있게 됩니다. 이제 model(tokenizer.encode("<snac_1_2048>"))같은 입력이 가능해집니다.

실무에서는 리사이징 후 즉시 몇 개의 샘플로 forward pass 테스트를 해서 에러가 없는지 확인하세요. 메모리 사용량은 약 100-200MB 증가하는데, 이는 새 임베딩 가중치 때문입니다.

실전 팁

💡 resize_token_embeddings()는 반드시 학습 전에 한 번만 호출하세요. 학습 중간에 호출하면 기존 학습이 모두 날아갑니다.

💡 리사이징 후 model.num_parameters()로 파라미터 개수를 확인하세요. 원본보다 약 1-2% 증가해야 정상입니다.

💡 임베딩 차원(hidden_size)은 변하지 않습니다. 오직 Vocabulary 크기만 증가합니다. 예: (256000, 2048) → (268290, 2048)

💡 리사이징 후 모델을 즉시 저장하세요. model.save_pretrained("./resized_model")로 저장하면 재사용 시 다시 리사이징할 필요가 없습니다.

💡 model.config.vocab_size가 자동으로 업데이트되는지 확인하세요. 일부 구현에서는 수동 업데이트가 필요할 수 있습니다.


6. 새로운 토큰 초기화 전략

시작하며

여러분이 12,000개 이상의 새 토큰을 추가했을 때, 이 토큰들의 임베딩 값을 어떻게 설정해야 할까요? 그냥 랜덤하게 초기화하면 될까요?

아니면 특별한 전략이 필요할까요? 이 질문이 바로 모델 성능을 좌우하는 핵심입니다.

이 초기화 전략은 학습 속도와 최종 품질에 엄청난 영향을 미칩니다. 잘못 초기화하면 수천 번의 iteration을 헤매거나, 최악의 경우 학습이 발산해서 실패할 수 있습니다.

특히 오디오처럼 텍스트와 완전히 다른 모달리티를 추가할 때는 더욱 신중해야 합니다. 바로 이럴 때 필요한 것이 체계적인 임베딩 초기화 전략입니다.

기존 토큰의 통계를 분석하고, 유사한 분포로 새 토큰을 초기화하여 학습의 안정성과 효율성을 극대화하는 방법을 알려드리겠습니다.

개요

간단히 말해서, 새 토큰 초기화는 모델에게 "새로운 단어를 배우는 출발점"을 제공하는 작업입니다. 좋은 출발점에서 시작하면 학습이 빠르고, 나쁜 출발점에서 시작하면 수렴이 느리거나 불가능합니다.

왜 단순 랜덤 초기화가 문제일까요? 기존 텍스트 토큰 임베딩은 수십억 개의 텍스트로 학습되어 특정 스케일과 분포를 가집니다.

예를 들어, 평균이 0이고 표준편차가 0.02 정도입니다. 새 토큰을 0.5 스케일로 초기화하면 모델이 혼란스러워하고 학습이 불안정해집니다.

기존에는 PyTorch 기본 초기화를 사용해서 학습이 느렸다면, 이제는 기존 임베딩의 통계를 복사하여 빠른 수렴이 가능합니다. 핵심 전략은 세 가지입니다: 기존 임베딩의 평균과 표준편차 계산, 동일한 분포로 새 토큰 초기화, 그리고 선택적으로 유사 토큰에서 복사하는 방법입니다.

특히 특수 토큰(<audio_start>)은 기존 특수 토큰(<bos>)과 유사하게 초기화하는 것이 효과적입니다.

코드 예제

import torch
from transformers import AutoModelForCausalLM

# 주석: 리사이징된 모델 로드
model = AutoModelForCausalLM.from_pretrained("./resized_model")
embeddings = model.get_input_embeddings().weight

# 주석: 기존 토큰 임베딩 통계 계산
original_vocab_size = 256000
original_embeddings = embeddings[:original_vocab_size]
mean = original_embeddings.mean().item()
std = original_embeddings.std().item()
print(f"기존 임베딩 평균: {mean:.6f}, 표준편차: {std:.6f}")

# 주석: 새 토큰 영역 초기화 (기존 분포 유지)
new_token_start = original_vocab_size
new_token_end = embeddings.shape[0]
with torch.no_grad():
    embeddings[new_token_start:new_token_end].normal_(mean=mean, std=std)

# 주석: 특수 토큰 초기화 (기존 <bos> 토큰 복사)
with torch.no_grad():
    bos_embedding = embeddings[tokenizer.bos_token_id]
    audio_start_id = tokenizer.convert_tokens_to_ids("<audio_start>")
    embeddings[audio_start_id] = bos_embedding.clone()

print("새 토큰 초기화 완료")

설명

이것이 하는 일: 위 코드는 새로 추가된 12,290개 토큰의 임베딩을 기존 토큰과 유사한 분포로 초기화합니다. 마치 새로운 학생에게 기존 학생들의 평균 성적 수준에서 시작하게 하는 것과 같습니다.

첫 번째로, 모델의 입력 임베딩 가중치 텐서를 가져옵니다. 이 텐서는 (268290, hidden_size) 크기를 가지며, 앞쪽 256,000개는 기존 토큰, 뒤쪽 12,290개는 새 토큰입니다.

기존 토큰 부분만 슬라이싱해서 평균과 표준편차를 계산합니다. 보통 평균은 0에 가깝고, 표준편차는 0.01~0.03 정도입니다.

그 다음으로, torch.no_grad() 컨텍스트 안에서 새 토큰 영역을 정규분포로 초기화합니다. normal_(mean, std) 메서드는 in-place로 텐서를 업데이트하며, 기존 토큰과 동일한 통계적 특성을 가지도록 합니다.

이렇게 하는 이유는 모델이 새 토큰을 기존 토큰과 유사한 방식으로 처리할 수 있도록 하기 위함입니다. 마지막으로, 특수 토큰인 <audio_start>의 경우 기존 <bos> 토큰의 임베딩을 복사합니다.

둘 다 "시작"을 나타내는 역할이므로 유사한 임베딩에서 시작하는 것이 학습에 유리합니다. .clone()을 사용해서 참조가 아닌 복사본을 만들어 독립적으로 학습될 수 있도록 합니다.

여러분이 이 초기화를 수행하면 학습 수렴 속도가 2-3배 빨라지고, 초기 loss가 훨씬 낮은 값에서 시작합니다. 실무에서는 초기화 후 몇 개 배치로 테스트 학습을 돌려서 loss가 정상 범위인지 확인하세요.

너무 높거나(발산) 너무 낮으면(초기화 문제) 재조정이 필요합니다.

실전 팁

💡 출력 임베딩(lm_head)도 동일하게 초기화하세요. 입력 임베딩만 하면 생성 품질이 떨어집니다.

💡 Xavier 초기화나 He 초기화보다 기존 분포 복사가 훨씬 효과적입니다. 전이학습에서는 일관성이 핵심입니다.

💡 초기화 후 임베딩을 freezing하지 마세요. 새 토큰은 반드시 학습되어야 의미를 획득합니다.

💡 레이어별로 다른 SNAC 토큰을 초기화할 때, 약간의 노이즈를 추가하세요. 완전히 동일하면 학습 초기에 구분이 안 됩니다.

💡 초기화 후 torch.save(embeddings, 'init_embeddings.pt')로 저장하세요. 여러 번 실험할 때 동일한 시작점을 보장할 수 있습니다.


#Python#LLM#Gemma#Vocabulary#TokenEmbedding#AI,TTS,음성합성,VoiceCloning,LLM,딥러닝

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.