Proje: Akademik makale PDF'lerinden (markdown'a çevrilmiş) yapılandırılmış metadata çıkarımı. Base Model: Qwen/Qwen2.5-14B-Instruct Eğitim Yöntemi: LoRA fine-tuning (Unsloth) Inference Motoru: vLLM (değerlendirme) Tarih Aralığı: Nisan 2026 — boyunca iteratif deneyler Donanım: TRUBA, 1x NVIDIA H100
TR Dizin akademik makalelerinden aşağıdaki alanları eksiksiz ve doğru JSON formatında çıkaran bir LLM elde etmek:
title.tr, title.enabstracts[] (dil + metin + anahtar kelimeler)authors[] (ad, ORCID, affiliation)doi, language, publicationType, docTypejournal.name, startPage, endPagereferences[] — en zorlu alan, 200'e kadar çıkabiliyor50_Operasyon/PDF-Cikarim-Araclari.md, benchmark: Deney 06.| Metrik | Değer |
|---|---|
| Toplam publication ID | 703,183 |
| Markdown elde edilen | 96,695 |
| Metadata JSON elde edilen | 96,695 |
| Fine-tuning dataset (toplam) | 95,340 |
| Train / Val / Test | 90,573 / 2,383 / 2,384 |
prepare_data.py şu pipeline'ı uyguladı:
ThreadPoolExecutor ile num_workers=32 işçi, boto3 client'ta max_pool_connections=64 + retries.output > 16K tokens olduğu için eğitim dışında tutuldu.| Ref sayısı | Örnek | Yüzde |
|---|---|---|
| 0-9 | 3,931 | 4.3% |
| 10-19 | 12,580 | 13.9% |
| 20-29 | 21,723 | 24.0% ← tepe |
| 30-39 | 19,109 | 21.1% |
| 40-49 | 12,546 | 13.8% |
| 50-59 | 7,918 | 8.7% |
| 60-69 | 4,986 | 5.5% |
| 70-79 | 3,140 | 3.5% |
| 80-89 | 1,902 | 2.1% |
| 90-99 | 1,136 | 1.3% |
| 100+ | 1,602 | 1.8% |
Dağılım sağlıklı: 20+ ref olan örnekler %81, 50+ ref %22.8. Eğitim sırasında modelin her boyda referans listesi görmesi sağlandı.
Sorun: Uzun markdown'lar (50K+ token) modelin max_seq_length=16384'üne sığmıyordu.
Çözüm: dynamic_smart_truncate_md — input token budget'ını output JSON tahminine göre dinamik hesapladı. Training sırasında output tam korundu, input orantılı kırpıldı.
Sorun: İlk veri hazırlama koşusunda metadata JSON'larını çekerken kullanılan kayıt sözlüğünde yanlış alan eşlemesi yapıldı. Bu hatanın somut yansıması:
title.tr ve title.en alanları, tezin gerçek başlığı
yerine boş / null değerlerle hedef JSON'a yazıldı.Bu hatanın değerlendirme aşamasındaki yansıması:
fuzzy_title.tr = 0.03, fuzzy_title.en = 0.07.expected_title alanı doğrudan kontrol edilince GT'de
null geldiği anlaşıldı → kök sebep veri hazırlama pipeline'ında bulundu.Çözüm:
prepare_data.py içinde metadata fetcher düzeltildi (doğru alan adı ve fallback
sırası: title.tr yoksa title["tr"], yoksa metadata'daki ana başlık).fuzzy_title.tr ve fuzzy_title.en her ikisi
de 0.03 / 0.07 → 1.00 seviyesine çıktı.fuzzy_title.tr = 1.00,
fuzzy_title.en = 1.00 (Bölüm 6.2).Çıkarılan ders: Metrik düşük geldiğinde modeli suçlamadan önce GT'yi doğrula. "Model bu alanı üretemiyor" hipotezi ile "GT'de bu alan zaten yok" hipotezi farklı yerlere bakar; tek satırlık bir sanity-check (örneklerden 5–10 tanesini gözle kontrol) bu yolu kısaltır.
Kısa bir compare_models.py ile Qwen2.5-14B, Qwen3-8B, Qwen3.5-9B zero-shot değerlendirildi.
| Model | Bağlam | Süre/örnek | JSON sağlamlığı |
|---|---|---|---|
| Qwen2.5-14B | 32K | ~90s | Orta |
| Qwen3-8B | 32K | ~30s | Düşük |
| Qwen3.5-9B | 128K | ~50s | Orta |
Gerekçeler:
config.yaml)model:
base_model: unsloth/Qwen2.5-14B-Instruct-bnb-4bit
max_seq_length: 16384
dtype: bfloat16
load_in_4bit: true
lora:
r: 32
alpha: 32
target_modules:
- q_proj, k_proj, v_proj, o_proj
- gate_proj, up_proj, down_proj
training:
num_train_epochs: 1
per_device_train_batch_size: 1
gradient_accumulation_steps: 8
learning_rate: 2e-4
warmup_ratio: 0.03
lr_scheduler_type: cosine
logging_steps: 500
save_steps: 500
eval_steps: 500
data:
total_samples: 100000
train_ratio: 0.95
val_ratio: 0.025
test_ratio: 0.025
Eğitim sonrası LoRA adapter → base model merge → checkpoints/merged (vLLM standart HF formatında yükleyebilsin diye).
Değerlendirme süreci 5+ iterasyon aldı, her biri yeni bir darboğaz ortaya çıkardı ve düzeltildi.
evaluate.py (transformers-native)Komut: python scripts/evaluate.py --num-samples 5
Sorun: ~10 dakika/örnek. Akıl almaz yavaş.
Teşhis:
do_sample=True (gereksiz stochastic decode)max_new_tokens=4096attention_mask verilmiyorDüzeltmeler:
tokenizer() ile explicit attention_masktemperature=0.0 (greedy decode)torch.inference_mode()use_cache=True, pad_token_id=tokenizer.eos_token_idSonuç: ~3 dk/örnek. Hâlâ çok yavaş.
Teşebbüs: pip install flash-attn --no-build-isolation
Hata: CUDA version (13.0) mismatches PyTorch (12.8)
Karar: FA2'yi tek başına yeniden kurmak yerine vLLM'e geç.
evaluate_vllm.py)vLLM içinde FA2 + continuous batching built-in. Ayrıca conda create -n vllm ile izole env kuruldu.
Yeni darboğaz: libstdc++ çakışması
ImportError: /lib64/libstdc++.so.6: version `CXXABI_1.3.15' not found
Çözüm:
export LD_LIBRARY_PATH="$CONDA_PREFIX/lib:$LD_LIBRARY_PATH"
Sonuç: ~20 sn/örnek. 30x hızlanma.
İlk test: 5 örnek, vLLM + 2-pass (genel metadata + ref-only)
Beklenmeyen sonuç:
avg_ref_recall = 0.18 (77 ref'li örnekte 15 ref yakalandı)avg_ref_precision bazı örneklerde 0.14'e düştüHatalı hipotez 1: "Training'de 77+ refli örnek görmedi"
Hatalı hipotez 2: "Context yetersiz, 32K'ya çıkaralım"
Gerçek teşhis (dönüm noktası):
REF_ONLY_PROMPT modeli eğitimde hiç görmedimax_new_tokens=2048 → JSON yarıda kesildiDüzeltme: 2-pass'i kaldır, tek pass'te büyük output budget:
--max-new-tokens 10240
--no-two-pass-refs
Sonuç (5 örnek):
avg_ref_recall = 0.938avg_ref_f1 = 0.933Komut:
python scripts/evaluate_vllm.py \
--max-model-len 24576 \
--max-new-tokens 10240 \
--no-two-pass-refs \
--num-samples 100
Sonuç:
json_validity_rate = 0.89 ← 11 parse hatası çıktı11 Hatanın Analizi: Hepsinin finish_reason="stop" olması kritik ipucu. Model üretimi tamamlıyordu, JSON yapısı da sağlam görünüyordu.
Tail'lerde tekrar eden pattern:
[\[CrossRef\]] # idx 43, 65, 85
[\[Link\]] # idx 65
[\[pubMed\]] # idx 65
[\[pMC\]] # idx 65
Kök sebep: Model markdown'dan \[ ve \] escape sequence'larını JSON string'inin içine alıyor. Ama JSON spec'te sadece şu escape'ler geçerli: \" \\ \/ \b \f \n \r \t \uXXXX.
Yani \[, \], \_, \-, \(, \), \* hepsi invalid JSON.
Düzeltme: _sanitize_json_escapes() fonksiyonu yazıldı. Karakter karakter gezip geçersiz backslash'leri atıyor:
\[ → [\] → ]\_ → _\" ve \n korunuyor (geçerli escape)Sonuç: 11 hatadan 10'u kurtarıldı → json_validity_rate = 0.99. Kalan 1 hata (#17) farklı bir sorun (muhtemelen orta yerde farklı karakter sorunu).
Metrikleri dikkatle okuyunca şu yapay düşüklükler fark edildi:
Title metrikleri düşük görünüyordu:
fuzzy_title.tr = 0.03, fuzzy_title.en = 0.07expected_title alanı doğrudan kontrol edildi → GT'nin
kendisi null geliyordu.Kök sebep — veri hazırlama bug'ı (bkz. Bölüm 2.5):
prepare_data.py içindeki metadata fetcher, kayıt sözlüğünde yanlış alan
eşlemesi nedeniyle title.tr / title.en alanlarını null olarak
hedef JSON'a yazıyordu.Düzeltme:
fuzzy_title.tr ve fuzzy_title.en her
ikisi de 1.00. Bu skor 100-örneklik doğrulamada ve 2384'lük tam test
setinde ayrı ayrı tekrar üretildi (Bölüm 6.2).DOI format farkları:
#16: 'DOI: 10.5505/ejm.2020.71676' — GT'de DOI: prefix varhttps://doi.org/..., doi:..., DOI: ...Fix: _normalize_doi() — prefix listesi + iteratif strip + lowercase + trailing noktalama.
Page format farkları:
Fix: _normalize_page() — leading zero strip, lowercase.
100-örneklik doğrulama sonrası:
| Metrik | Önce | Sonra |
|---|---|---|
fuzzy_title.tr |
0.07 | 1.00 |
fuzzy_title.en |
0.03 | 1.00 |
exact_doi |
0.76 | 0.77 |
overall_score |
4.94 | 5.35 |
DOI/Page'deki marjinal artış, title tarafındaki ana problemin veri eksikliği değil; önceki aşamada parse/okuma kaynaklı yanlış değerlendirme olduğunu gösterdi.
sbatch run_eval_vllm.slurm
# Tüm test seti (2384 örnek), 32K context, 14K output, single-pass
Konfigürasyon:
max_model_len = 32768max_new_tokens = 14336dtype = bfloat16, gpu_memory_utilization = 0.9temperature = 0.0 (greedy)Metrikler (n = 2384):
| Kategori | Metrik | Değer | Yorum |
|---|---|---|---|
| JSON | json_validity_rate |
0.9807 | %1.93 hata (46/2384) |
| Başlık | fuzzy_title.tr |
1.0000 | Metadata fetch bug'ı düzeltildikten sonra (bkz. 2.5) |
| Başlık | fuzzy_title.en |
1.0000 | Metadata fetch bug'ı düzeltildikten sonra (bkz. 2.5) |
| Yazarlar | author_count_match |
0.9705 | İyi |
| Yazarlar | author_names_avg |
0.9107 | İyi |
| Meta | exact_docType |
0.9769 | Çok iyi |
| Meta | exact_publicationType |
0.8654 | İyi |
| Meta | exact_language |
0.8691 | İyi |
| Özet | abstract_count_match |
0.7789 | Orta |
| Özet | abstract_lang_match |
0.9850 | Çok iyi |
| Dergi | fuzzy_journal.name |
0.6493 | Düşük |
| DOI | exact_doi |
0.6683 | Düşük |
| Sayfa | exact_startPage |
0.6382 | Düşük |
| Sayfa | exact_endPage |
0.6262 | Düşük |
| ORCID | orcid_recall |
0.6081 | Orta |
| Referans | ref_f1 |
0.8371 | İyi |
| Referans | ref_precision |
0.8665 | İyi |
| Referans | ref_recall |
0.8307 | İyi |
| Referans | ref_count_match |
0.6209 | Orta (tam sayı) |
| Referans | ref_count_tol2 |
0.7807 | ±2 içinde |
| Referans | ref_exact_all |
0.4287 | %43 birebir |
| Referans | avg_ref_count_pred |
32.26 | Eksik |
| Referans | avg_ref_count_exp |
37.35 | — |
| Referans | avg_ref_count_diff |
7.39 | Ortalama eksik/fazla |
| Skor | overall_score |
5.2456 | — |
| Perf. | avg_time_per_sample_sec |
6.46 | ~4.3 saat toplam |
docType, authors, abstract_lang > %90finish=stop olduğu için token limit problemi değil. Muhtemelen hâlâ yakalanamayan özel karakter kombinasyonları var.ref_count_tol2 = 0.78 → %78'inde ±2 içinde, yani çoğu durum kabul edilebilir.A) Parse hatalarını 0'a indir (2384'te 46 → 5'ten az hedef)
error_examples'daki 46 hatanın tail'lerini analiz etjson_validity 0.98 → 0.995+B) DOI/Page tarafında model çıktısını net gör
evaluate_vllm.py'ye predicted_doi, predicted_startPage, predicted_endPage alanlarını kaydetC) Referansları daha tamamlayıcı yap
avg_ref_count_diff = 7.39 ile ortalama 7 ref eksik/fazla veriyorD) Veri hazırlama sanity-check'i (metadata fetch bug'ının tekrarını önle)
title.tr / title.en alanlarının null gelmesinden kaynaklandı (Bölüm 2.5).
Düzeltildikten sonra skor 1.00'a çıktı.prepare_data.py koşusunun sonunda alan-bazlı null-rate
raporu üret (title.tr null %, doi null %, journal null %...).
Bir alan beklenenden çok daha boş geliyorsa pipeline crash etsin / uyarı
versin.expected_* doğrulaması zorunlu adım olsun.evaluate_vllm.py çıktısına predicted_title ve expected_title örnekleri
yazılsın; metrik düşükse hızlıca GT mi model mi sorusu cevaplanabilsin.E) ORCID recall (0.61) iyileştirme
F) Journal name fuzzy için alias tablosu
G) Aktif öğrenme ile zor örnekleri retraining
ref_count_diff > 15 veya json_valid=False olan test örneklerini toplaH) Ensembling / self-consistency
temperature=0.3 ile çalıştır, majority voteI) Chunked ref extraction
LD_LIBRARY_PATH sistem vs conda çakışması sık yaşanırexpected_* alanına gözle bakmak bu yolu çok kısaltıyor.finish_reason her inference sonucunda kontrol edilmeli (length vs stop ayrımı çok şey söylüyor)title.tr / title.en alanları null geldi; bu durum eğitim sinyalini kirletti ve değerlendirmede de yanıltıcı düşük skor üretti (Bölüm 2.5). Düzeltme sonrası başlık skorları 1.00'a çıktı.cd /arf/scratch/alisahin7/Qwen_Fine_Tune
conda activate qwen_ft
python scripts/prepare_data.py --config config/config.yaml
sbatch --export=ALL,STAGE=train run_qwen_finetune.slurm
# Training sonrası otomatik, ya da manuel:
python scripts/merge_lora.py \
--base-model unsloth/Qwen2.5-14B-Instruct \
--lora-path checkpoints/lora_final \
--output checkpoints/merged
conda activate vllm
export LD_LIBRARY_PATH="$CONDA_PREFIX/lib:$LD_LIBRARY_PATH"
# Tüm test seti
sbatch run_eval_vllm.slurm
# Veya sadece N örnek
sbatch --export=ALL,NUM_SAMPLES=500 run_eval_vllm.slurm
python scripts/inference.py \
--model-path checkpoints/merged \
--input some_paper.md \
--output result.json
Qwen2.5-14B tabanlı fine-tuned model, TR Dizin metadata extraction için production-ready bir seviyede. Özellikle:
Son tavsiye: Mevcut model canlıya alınabilir. Paralelde:
Rapor Tarihi: 2026-04-22 Yazan: Ali Şahin + AI asistan Versiyon: 1.0