İçeriğe geç

Vocabulary Extension: Llama-3 Tokenizer'a 8K TR Token Ekle (Embedding Init Stratejileri)

Llama-3 default tokenizer 128K — multilingual ama TR için verim düşük. 'Extension' yaklaşımı: Llama-3 vocab'ına 8K TR-spesifik token ekle, embedding matrix'i 128K→136K büyüt, yeni satırları akıllıca init et (mean-init, SVD-init, byte-decomposition). RTX 4090'da pratik lab + perplexity delta ölçümü.

Şükrü Yusuf KAYA
32 dakikalık okuma
İleri
Vocabulary Extension: Llama-3 Tokenizer'a 8K TR Token Ekle (Embedding Init Stratejileri)
🎯 Hedef
Llama-3'ün 128K multilingual vocab'ı TR'de 3.2 tokens/word. Bu Lab ile 136K vocab → 2.1 tokens/word. Önemli: yeni token'lar için embedding ağırlıkları akıllıca initialize edilmeli, yoksa model bu token'lara 'çöp' atar.

1. Niye Sıfırdan Yeni Tokenizer Değil de Extension?#

Sıfırdan TR-only 50K tokenizer eğitirsen:
  • Tüm Llama-3 weights bozulur (mismatched vocab)
  • TR-only model — İngilizce capability sıfır
  • Pre-training compute büyük
Extension:
  • Llama-3 weights korunur (TR + EN capability)
  • Sadece TR-spesifik 8K token eklenir
  • Yeni token embedding'leri akıllıca init edilirse minimal düzeltme yeter
  • Continual pre-train veya doğrudan SFT ile adapte olur
python
# === Llama-3 Tokenizer Extension Lab ===
from transformers import AutoTokenizer
from tokenizers import Tokenizer
 
# 1. Base tokenizer'ı yükle
base = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B")
print(f"Base vocab size: {base.vocab_size}") # 128256
 
# 2. TR-spesifik tokenizer'ı yükle (önceki dersten)
tr_tok = Tokenizer.from_file("tr_bpe_50k.json")
tr_vocab = tr_tok.get_vocab() # {token_str: id}
 
# 3. Aday TR token'ları seç: Llama-3'te olmayan, TR-corpus'ta sık
tr_corpus_sample = open("/data/tr-corpus/oscar-tr.txt").read()[:50_000_000] # 50MB sample
 
from collections import Counter
# Tokenize sample with TR tokenizer
tr_tokens_in_corpus = [t for t in tr_tok.encode(tr_corpus_sample).tokens]
tr_token_freq = Counter(tr_tokens_in_corpus)
 
# Base tokenizer'ın vocab'ında olmayanları filtre
base_vocab_set = set(base.get_vocab().keys())
new_candidates = []
for tok, freq in tr_token_freq.most_common():
if tok not in base_vocab_set and freq >= 50:
new_candidates.append((tok, freq))
if len(new_candidates) >= 8000:
break
 
print(f"Aday TR-spesifik token sayısı: {len(new_candidates)}")
 
# 4. Base tokenizer'a ekle
new_tokens = [tok for tok, _ in new_candidates]
n_added = base.add_tokens(new_tokens)
print(f"Eklenen: {n_added}, Yeni vocab: {len(base)}") # 128256 + 8000 = 136256
 
# 5. Kaydet
base.save_pretrained("llama3-tr-tok-136k")
Llama-3 + 8K TR token extension
python
# === Embedding Init Stratejileri ===
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
 
ext_tok = AutoTokenizer.from_pretrained("llama3-tr-tok-136k")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Meta-Llama-3.1-8B",
torch_dtype=torch.bfloat16, device_map="cuda")
 
# Mevcut embedding 128256 satır
old_emb = model.get_input_embeddings().weight.data
old_lm_head = model.get_output_embeddings().weight.data
old_vocab = 128256
new_vocab = len(ext_tok)
print(f"old emb shape: {old_emb.shape}, new vocab: {new_vocab}")
 
# 6. Embedding'i resize et
model.resize_token_embeddings(new_vocab)
new_emb = model.get_input_embeddings().weight.data
new_lm_head = model.get_output_embeddings().weight.data
 
# 7. STRATEJİ A — Mean-init (en basit)
mean_vec = old_emb.mean(dim=0, keepdim=True) # [1, hidden]
new_emb[old_vocab:] = mean_vec.expand(new_vocab - old_vocab, -1)
new_lm_head[old_vocab:] = old_lm_head.mean(dim=0, keepdim=True).expand(new_vocab - old_vocab, -1)
 
# 7b. STRATEJİ B — Byte-decomposition init (en akıllı)
# Yeni TR token "şirket" → base tokenizer'da ["ş", "i", "r", "k", "e", "t"] olarak parçalanabilir
# Yeni embedding'i = ortalama(parça embedding'leri) yap
base = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3.1-8B")
for new_id, tok_str in enumerate(ext_tok.convert_ids_to_tokens(range(old_vocab, new_vocab))):
# base tokenizer ile yeni token'ı parçala
decomp_ids = base.encode(tok_str.replace("Ġ", " "), add_special_tokens=False)
if decomp_ids:
new_emb[old_vocab + new_id] = old_emb[decomp_ids].mean(dim=0)
new_lm_head[old_vocab + new_id] = old_lm_head[decomp_ids].mean(dim=0)
 
# 7c. STRATEJİ C — SVD-init (research)
# Old embedding matrix'in low-rank yaklaşımıyla yeni vektör çıkar
U, S, V = torch.svd(old_emb.float())
# Yeni embedding'ler için random combination of top-k singular vectors
torch.manual_seed(42)
k = 32
for new_id in range(old_vocab, new_vocab):
coeffs = torch.randn(k) * 0.02
new_emb[new_id] = (U[:, :k] @ (S[:k] * coeffs)).bfloat16()
 
# 8. Save
model.save_pretrained("llama3-tr-emb-136k")
ext_tok.save_pretrained("llama3-tr-emb-136k")
embedding init: mean / byte-decomposition / SVD

2. Perplexity Delta Ölçümü#

Hangi init stratejisinin pratik etkisi var? 1000 TR cümle üzerinde perplexity:
import torch from torch.nn.functional import cross_entropy @torch.no_grad() def perplexity(model, tokenizer, text, device="cuda"): enc = tokenizer(text, return_tensors="pt").to(device) out = model(**enc, labels=enc["input_ids"]) return torch.exp(out.loss).item() # Test texts = open("/data/tr-eval-1k.txt").readlines() for init_strategy in ["mean", "byte_decomp", "svd"]: model = AutoModelForCausalLM.from_pretrained(f"llama3-tr-emb-136k-{init_strategy}", torch_dtype=torch.bfloat16, device_map="cuda") ppls = [perplexity(model, ext_tok, t) for t in texts[:100]] print(f"{init_strategy:15s} median PPL: {sorted(ppls)[50]:.2f}")
Tipik sonuç (Llama-3.1-8B + extension, continual pre-train yapmadan):
StratejiMedian PPLYorum
mean-init2843tüm yeni token'lar aynı vector → kötü
byte-decomp38.4parça embedding ortalaması, yarı-anlamlı
svd-init124rastgele combination, orta
base Llama (no extension)18.2reference (tüm TR'yi parçalı tokenize ediyor)
Extension önce perplexity yükseliyor (yeni token'lar henüz öğrenilmedi). Continual pre-train (1-5B token) ile 18-22 PPL'a iniyor → base'i geçiyor.

3. Continual Pre-train Maliyeti (Cookbook Sayıları)#

RTX 4090 + Llama-3.1 8B + 8K extension:
  • Hedef: 2B token TR continual pre-train
  • Effective batch 64K token (packing + grad-accum)
  • 2B / 64K = 31,250 step
  • step/s ≈ 1.5 (8B, QLoRA-style adapter only on extension)
  • Wall-clock: 31,250 / 1.5 / 3600 = 5.8 saat
  • Elektrik maliyet: ~₺10
Cloud (8×H100): ~1 saat → $24.
Cookbook'un kuralı: TR extension yapıyorsan mutlaka 1-2B token continual pre-train. Aksi halde model yeni token'ları yorumlayamaz, downstream SFT'de loss yüksek başlar.
🐛 FMD — 'Extension yaptım, SFT'de loss 4.5'ten başlıyor (normal 2.5)'
Hipotez: Embedding init zayıf, yeni token'lar 'rastgele' vector → SFT gradient bunları çekmeye çalışıyor ama 8B parametrenin geri kalanı zaten kararlı → loss yüksek. Çözüm: continual pre-train 1B token önce, sonra SFT. Veya: byte-decomposition init kullan (mean'den çok daha iyi). Drill: 3 init stratejisiyle SFT loss curve'lerini overlay et.
✅ Teslim
  1. 8K TR token ekle, 3 init stratejisi dene. 2) Perplexity delta'sını ölç. 3) Byte-decomp init + 500M token continual pre-train sonrası downstream SFT'de loss curve gör. 4) Sonraki ders: 2.3 — Tokenizer Distillation: Çoklu Modeller Arası Mapping.

Yorumlar & Soru-Cevap

(0)
Yorum yazmak için giriş yap.
Yorumlar yükleniyor...

İlgili İçerikler