Fine-Tuning von Sprachmodellen mit domänenspezifischen Daten
Feintuning von Sprachmodellen mit LoRA und QLoRA: Datensatzvorbereitung, Trainingsstrategien und Evaluierung — Spezialisten ohne riesiges Compute-Budget.

Allgemeine Sprachmodelle wissen ein wenig über alles. Sie können Gedichte schreiben, Quantenphysik erklären und Code generieren. Aber frag sie nach den internen APIs deines Unternehmens, der branchenspezifischen Terminologie oder den Coding Conventions deiner Organisation, und sie halluzinieren selbstbewusst. Fine-Tuning verwandelt einen Generalisten in einen Spezialisten – ein Modell, das die Sprache, Muster und Einschränkungen deiner Domäne versteht.
Die Hürde für Fine-Tuning ist gesunken. Parameter-effiziente Methoden wie LoRA erlauben es, ein 7B-Parameter-Modell auf einer einzelnen GPU in wenigen Stunden zu fine-tunen. Du brauchst weder ein Team von ML-Engineern noch einen Cluster aus A100s. Du brauchst gute Daten, klare Ziele und die Disziplin zu prüfen, ob dein fine-getuntes Modell für deinen konkreten Anwendungsfall tatsächlich besser ist als das Basismodell.
Wann Fine-Tuning sinnvoll ist
Fine-Tuning ist nicht immer die Antwort. Bevor du in Training investierst, verstehe, wann es das richtige Werkzeug ist.
## Decision framework
### Use fine-tuning when:
- You need the model to adopt a specific style or format
(medical reports, legal briefs, code in your framework)
- Domain-specific terminology that the base model gets wrong
- You have 100+ high-quality examples of desired behavior
- Prompt engineering alone can't achieve consistent quality
- Latency matters and you can't afford long prompts with
many examples
### Use RAG (retrieval) instead when:
- The knowledge changes frequently (product catalog, docs)
- You need citations and verifiable sources
- The base model understands the domain but lacks specifics
- You have structured data that's easy to retrieve
### Use prompt engineering instead when:
- You have fewer than 50 examples
- The task is well-defined and consistent
- Few-shot examples in the prompt achieve good results
- You need flexibility to change behavior without retrainingVorbereitung der Trainingsdaten
Die Qualität deines Fine-Tuning-Datensatzes bestimmt die Qualität deines Modells. Schlechte Eingaben führen zu schlechten Ausgaben – hier gilt das mehr als an jeder anderen Stelle in der Software.
# Dataset preparation for instruction fine-tuning
import json
from typing import TypedDict
class TrainingExample(TypedDict):
instruction: str
input: str
output: str
# ❌ Low-quality training data
bad_examples = [
{
"instruction": "Write code",
"input": "make a function",
"output": "def foo(): pass" # Too vague, teaches nothing
},
{
"instruction": "Fix the bug",
"input": "it doesn't work",
"output": "try restarting" # Not actionable
},
]
# ✅ High-quality training data
good_examples: list[TrainingExample] = [
{
"instruction": (
"Write a TypeScript function that validates "
"an email address using our company's domain rules."
),
"input": (
"Requirements: Must end with @company.com or "
"@subsidiary.com. Must not contain plus addressing. "
"Must be lowercase."
),
"output": (
"function validateCompanyEmail(email: string): "
"{ valid: boolean; error?: string } {\n"
" const normalized = email.toLowerCase().trim();\n"
" \n"
" if (normalized !== email) {\n"
" return { valid: false, error: "
"'Email must be lowercase' };\n"
" }\n"
" \n"
" if (normalized.includes('+')) {\n"
" return { valid: false, error: "
"'Plus addressing not allowed' };\n"
" }\n"
" \n"
" const allowedDomains = "
"['@company.com', '@subsidiary.com'];\n"
" const hasValidDomain = allowedDomains.some(\n"
" domain => normalized.endsWith(domain)\n"
" );\n"
" \n"
" if (!hasValidDomain) {\n"
" return { valid: false, error: "
"'Must use company email domain' };\n"
" }\n"
" \n"
" return { valid: true };\n"
"}"
),
},
]
def prepare_dataset(
examples: list[TrainingExample],
output_path: str
) -> None:
"""Format examples for fine-tuning frameworks."""
formatted = []
for ex in examples:
formatted.append({
"messages": [
{
"role": "system",
"content": (
"You are a senior engineer at Company. "
"Follow our coding standards and conventions."
),
},
{
"role": "user",
"content": f"{ex['instruction']}\n\n{ex['input']}",
},
{
"role": "assistant",
"content": ex["output"],
},
]
})
with open(output_path, "w") as f:
for item in formatted:
f.write(json.dumps(item) + "\n")LoRA: Fine-Tuning ohne die GPU-Rechnung
Low-Rank Adaptation (LoRA) friert die ursprünglichen Modellgewichte ein und trainiert kleine Adapter-Matrizen. Das reduziert die trainierbaren Parameter um 99%+ und erreicht dennoch eine vergleichbare Qualität wie ein vollständiges Fine-Tuning.
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model, TaskType
from datasets import load_dataset
from trl import SFTTrainer, SFTConfig
# Load base model
model_name = "meta-llama/Llama-3.1-8B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# LoRA configuration
# r: rank of the adaptation matrices (lower = fewer params)
# alpha: scaling factor (typically 2x rank)
# target_modules: which layers to adapt
# ❌ Too aggressive: high rank, all modules
bad_config = LoraConfig(
r=128, # Way too many parameters
lora_alpha=256,
target_modules="all-linear", # Unnecessary
)
# ✅ Balanced: enough capacity for domain adaptation
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
bias="none",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 13,631,488 || all params: 8,043,167,744
# trainable%: 0.1695Trainings-Loop und Hyperparameter
Die Trainingskonfiguration ist wichtiger, als die meisten realisieren. Lernrate, Batch-Größe und Anzahl der Epochen können den Unterschied zwischen einem nützlichen Modell und einem katastrophal überanpassten ausmachen.
# Training configuration
training_config = SFTConfig(
output_dir="./checkpoints",
# Learning rate: too high = catastrophic forgetting
# too low = you're just wasting compute
learning_rate=2e-4,
# Warmup prevents early instability
warmup_steps=100,
lr_scheduler_type="cosine",
# Epochs: 1-3 for most datasets
# More epochs = more overfitting risk
num_train_epochs=2,
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # Effective batch size: 16
# Evaluation during training to catch overfitting
eval_strategy="steps",
eval_steps=50,
save_strategy="steps",
save_steps=50,
# Mixed precision for memory efficiency
bf16=True,
logging_steps=10,
max_seq_length=2048,
)
# Load dataset
dataset = load_dataset("json", data_files={
"train": "data/train.jsonl",
"eval": "data/eval.jsonl",
})
# Initialize trainer
trainer = SFTTrainer(
model=model,
args=training_config,
train_dataset=dataset["train"],
eval_dataset=dataset["eval"],
processing_class=tokenizer,
)
# Train
trainer.train()
# Save the adapter (not the full model)
trainer.save_model("./final-adapter")
# Adapter is ~50MB vs 16GB for the full modelEvaluation: Hilft das fine-getunte Modell wirklich?
Der wichtigste Schritt ist eine rigorose Evaluation. Ein Modell, das beim Trainings-Loss gut abschneidet, aber schlechtere Outputs liefert als das Basismodell mit gutem Prompting, ist Zeitverschwendung.
# Evaluation framework
from dataclasses import dataclass
@dataclass
class EvalResult:
example_id: str
base_output: str
finetuned_output: str
reference_output: str
base_score: float
finetuned_score: float
def evaluate_model(
base_model,
finetuned_model,
eval_examples: list[dict],
scorer,
) -> list[EvalResult]:
"""Compare base vs fine-tuned on held-out examples."""
results = []
for example in eval_examples:
prompt = example["instruction"] + "\n" + example["input"]
base_output = generate(base_model, prompt)
ft_output = generate(finetuned_model, prompt)
base_score = scorer.score(
base_output, example["output"]
)
ft_score = scorer.score(
ft_output, example["output"]
)
results.append(EvalResult(
example_id=example["id"],
base_output=base_output,
finetuned_output=ft_output,
reference_output=example["output"],
base_score=base_score,
finetuned_score=ft_score,
))
# Summary statistics
base_avg = sum(r.base_score for r in results) / len(results)
ft_avg = sum(r.finetuned_score for r in results) / len(results)
print(f"Base model average: {base_avg:.3f}")
print(f"Fine-tuned average: {ft_avg:.3f}")
print(f"Improvement: {(ft_avg - base_avg) / base_avg * 100:.1f}%")
# Check for regressions: cases where fine-tuned is worse
regressions = [
r for r in results
if r.finetuned_score < r.base_score
]
print(f"Regressions: {len(regressions)}/{len(results)}")
return resultsHäufige Fallstricke
# ❌ Pitfall 1: Overfitting on small datasets
# Symptom: training loss drops to near zero,
# eval loss increases
# Fix: fewer epochs, more dropout, more data
# ❌ Pitfall 2: Catastrophic forgetting
# Symptom: domain task improves but general
# capability degrades
# Fix: lower learning rate, mix in general data (5-10%)
# ❌ Pitfall 3: Poor data quality
# Symptom: model learns your mistakes and inconsistencies
# Fix: have domain experts review training examples
# Quality check: would you want a junior dev to
# learn from this example?
# ✅ Practical data quality checklist:
# - Every example has a clear, complete answer
# - No contradictions between examples
# - Consistent formatting and style
# - At least 100 examples for narrow tasks
# - At least 500 for broader domain adaptation
# - 10-20% held out for evaluationWichtige Erkenntnisse
Fine-Tuning ist die richtige Wahl, wenn du konsistenten Stil, Format oder Domänen-Terminologie brauchst, die Prompting allein nicht zuverlässig erreicht – aber wenn sich dein Wissen häufig ändert oder du Zitate brauchst, ist Retrieval-Augmented Generation (RAG) meist die bessere Wahl. LoRA macht Fine-Tuning auf Consumer-Hardware zugänglich, indem es winzige Adapter-Matrizen trainiert (0,17% der Gesamtparameter) und dabei Ergebnisse liefert, die mit vollständigem Fine-Tuning vergleichbar sind – starte mit Rank 16, ziele auf die Attention- und Feed-Forward-Projektionsschichten ab und verwende einen Cosinus-Lernraten-Schedule mit Start bei 2e-4. Datenqualität zählt mehr als Quantität: 200 von Experten kuratierte Beispiele, bei denen jedes genau das gewünschte Verhalten demonstriert, schlagen 10.000 verrauschte gescrapte Beispiele, und jedes Trainingsbeispiel sollte den Test "würde ich wollen, dass ein Junior-Entwickler davon lernt?" bestehen. Evaluiere immer gegen das Basismodell auf ausgelagerten Beispielen, bevor du deployst, denn ein fine-getuntes Modell, das beim Trainings-Loss gut abschneidet, aber schlechtere Outputs als das Basismodell mit gutem Prompting produziert, hat negativen Wert – prüfe auf Regressionen und katastrophales Vergessen, nicht nur auf durchschnittliche Verbesserung.


