Zum Inhalt springen

Fine-Tuning von Sprachmodellen für domänenspezifische Aufgaben

Praktischer Leitfaden zum Fine-Tuning von Sprachmodellen auf Domänendaten: Datensätze, Trainingsstrategien, Evaluation und Bereitstellung.

5 Min. Lesezeit
Diagramm, das zeigt, wie ein vortrainiertes Sprachmodell mit domänenspezifischen Daten gefinetuned wird, um spezialisierte Ausgaben für eine Zielaufgabe zu erzeugen

Vortrainierte Sprachmodelle erfassen allgemeines Sprachverständnis aus riesigen Korpora, verfügen aber nicht über die Spezifität, die deine Domäne erfordert. Ein Modell, das mit Internettext trainiert wurde, versteht weder die interne Terminologie deines Unternehmens, die regulatorische Sprache deiner Branche noch die spezielle Nomenklatur deines Produkts. Fine-Tuning schließt diese Lücke, indem es ein allgemeines Modell so anpasst, dass es mit deinen spezifischen Daten bei deiner spezifischen Aufgabe gut abschneidet.

Die Frage ist nicht, ob man fine-tunen sollte—sondern wie man es effizient macht, ohne Overfitting, katastrophales Vergessen oder das Compute-Budget zu verbrennen.

Datensatzvorbereitung und -qualität

Die Qualität deines Fine-Tuning-Datensatzes bestimmt die Leistungsobergrenze deines Modells. "Garbage in, garbage out" gilt hier mit besonderer Kraft.

pypython
# ❌ Throwing raw data at the model without cleaning
training_data = [
    {"text": doc["content"], "label": doc["category"]}
    for doc in raw_documents
]
# No deduplication, no quality filtering, no validation
pypython
# ✅ Structured dataset preparation pipeline
import hashlib
from dataclasses import dataclass
from typing import Optional
 
 
@dataclass
class TrainingExample:
    input_text: str
    output_text: str
    source: str
    quality_score: float
 
 
class DatasetPipeline:
    def __init__(self, min_quality: float = 0.7,
                 min_length: int = 50, max_length: int = 2048):
        self.min_quality = min_quality
        self.min_length = min_length
        self.max_length = max_length
        self._seen_hashes: set[str] = set()
 
    def process(
        self, raw_examples: list[dict]
    ) -> list[TrainingExample]:
        processed = []
 
        for raw in raw_examples:
            example = self._clean(raw)
            if example is None:
                continue
 
            if not self._passes_quality(example):
                continue
 
            if self._is_duplicate(example):
                continue
 
            processed.append(example)
 
        return processed
 
    def _clean(self, raw: dict) -> Optional[TrainingExample]:
        text = raw.get("input", "").strip()
        output = raw.get("output", "").strip()
 
        if not text or not output:
            return None
 
        # Normalize whitespace
        text = " ".join(text.split())
        output = " ".join(output.split())
 
        return TrainingExample(
            input_text=text,
            output_text=output,
            source=raw.get("source", "unknown"),
            quality_score=raw.get("quality", 0.5),
        )
 
    def _passes_quality(self, example: TrainingExample) -> bool:
        if example.quality_score < self.min_quality:
            return False
        if len(example.input_text) < self.min_length:
            return False
        if len(example.input_text) > self.max_length:
            return False
        return True
 
    def _is_duplicate(self, example: TrainingExample) -> bool:
        content_hash = hashlib.sha256(
            example.input_text.encode()
        ).hexdigest()
 
        if content_hash in self._seen_hashes:
            return True
 
        self._seen_hashes.add(content_hash)
        return False

Deduplizierung ist wichtiger, als du vielleicht erwartest. Reale Datensätze enthalten oft nahezu identische Einträge, die das Modell dazu verleiten, spezifische Muster auswendig zu lernen, anstatt generalisierbares Verhalten zu erlernen.

Trainingskonfiguration und Hyperparameter

Fine-Tuning erfordert andere Hyperparameter als Pre-Training. Die Lernrate muss niedrig genug sein, um das vortrainierte Wissen zu bewahren, und hoch genug, um neue Muster zu lernen.

pypython
from transformers import (
    AutoModelForSequenceClassification,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
)
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingLR
 
 
def create_training_config(
    model_name: str,
    output_dir: str,
    num_examples: int,
    batch_size: int = 8,
) -> TrainingArguments:
    # Calculate steps based on dataset size
    steps_per_epoch = num_examples // batch_size
    total_epochs = 3  # Fine-tuning rarely needs more
    warmup_steps = int(steps_per_epoch * 0.1)
 
    return TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=total_epochs,
        per_device_train_batch_size=batch_size,
        per_device_eval_batch_size=batch_size * 2,
        learning_rate=2e-5,  # Much lower than pre-training
        weight_decay=0.01,
        warmup_steps=warmup_steps,
        lr_scheduler_type="cosine",
        evaluation_strategy="steps",
        eval_steps=steps_per_epoch // 2,
        save_strategy="steps",
        save_steps=steps_per_epoch,
        load_best_model_at_end=True,
        metric_for_best_model="eval_loss",
        logging_steps=50,
        fp16=True,
        gradient_accumulation_steps=4,
        dataloader_num_workers=4,
    )
 
 
def fine_tune(
    model_name: str,
    train_dataset,
    eval_dataset,
    output_dir: str,
):
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForSequenceClassification.from_pretrained(
        model_name, num_labels=train_dataset.num_labels
    )
 
    config = create_training_config(
        model_name=model_name,
        output_dir=output_dir,
        num_examples=len(train_dataset),
    )
 
    trainer = Trainer(
        model=model,
        args=config,
        train_dataset=train_dataset,
        eval_dataset=eval_dataset,
        tokenizer=tokenizer,
    )
 
    trainer.train()
    return trainer

Eine Lernrate von 2e-5 ist ein gut etablierter Ausgangspunkt für das Fine-Tuning von Transformern. Eine deutlich höhere Rate riskiert katastrophales Vergessen: das Modell verliert seine vortrainierten Fähigkeiten. Eine deutlich niedrigere Rate verschwendet Compute, ohne ausreichende Anpassung zu ermöglichen.

Parameter-effizientes Fine-Tuning mit LoRA

Vollständiges Fine-Tuning aktualisiert jeden Parameter des Modells, was teuer und oft unnötig ist. LoRA (Low-Rank Adaptation) friert die ursprünglichen Gewichte ein und injiziert kleine trainierbare Matrizen, wodurch sich der Speicherbedarf drastisch reduziert.

pypython
from peft import LoraConfig, get_peft_model, TaskType
 
 
def create_lora_model(base_model_name: str, task_type: str):
    """Create a LoRA-adapted model for efficient fine-tuning."""
    from transformers import AutoModelForCausalLM
 
    base_model = AutoModelForCausalLM.from_pretrained(
        base_model_name,
        load_in_8bit=True,  # Quantize base model
        device_map="auto",
    )
 
    lora_config = LoraConfig(
        r=16,  # Rank of the low-rank matrices
        lora_alpha=32,  # Scaling factor
        target_modules=[
            "q_proj", "k_proj", "v_proj", "o_proj",
        ],
        lora_dropout=0.05,
        bias="none",
        task_type=TaskType.CAUSAL_LM,
    )
 
    model = get_peft_model(base_model, lora_config)
 
    # Show parameter efficiency
    trainable = sum(
        p.numel() for p in model.parameters() if p.requires_grad
    )
    total = sum(p.numel() for p in model.parameters())
    print(
        f"Trainable: {trainable:,} / {total:,} "
        f"({100 * trainable / total:.2f}%)"
    )
    # Typically: 0.1-1% of total parameters
 
    return model

LoRA trainiert typischerweise weniger als 1% der Gesamtparameter und erreicht dennoch eine Leistung vergleichbar mit vollständigem Fine-Tuning. Das macht es praktikabel, große Modelle auf Consumer-GPUs zu finetunen.

Evaluation jenseits von Loss-Kurven

Sinkende Trainingsloss bedeutet nicht automatisch, dass dein Modell nützlich ist. Du brauchst aufgabenspezifische Evaluationsmetriken, die die reale Leistung widerspiegeln.

pypython
from sklearn.metrics import (
    classification_report,
    confusion_matrix,
)
import numpy as np
 
 
class DomainEvaluator:
    def __init__(self, label_names: list[str]):
        self.label_names = label_names
        self.predictions: list[int] = []
        self.references: list[int] = []
 
    def add_batch(
        self, predictions: np.ndarray, references: np.ndarray
    ):
        self.predictions.extend(predictions.tolist())
        self.references.extend(references.tolist())
 
    def compute_metrics(self) -> dict:
        report = classification_report(
            self.references,
            self.predictions,
            target_names=self.label_names,
            output_dict=True,
        )
 
        cm = confusion_matrix(
            self.references, self.predictions
        )
 
        # Find worst-performing classes
        per_class = {
            name: report[name]["f1-score"]
            for name in self.label_names
        }
        worst_classes = sorted(
            per_class.items(), key=lambda x: x[1]
        )[:3]
 
        return {
            "macro_f1": report["macro avg"]["f1-score"],
            "weighted_f1": report["weighted avg"]["f1-score"],
            "per_class_f1": per_class,
            "worst_classes": worst_classes,
            "confusion_matrix": cm.tolist(),
        }
 
    def detect_regression(
        self,
        baseline_metrics: dict,
        threshold: float = 0.02,
    ) -> list[str]:
        """Flag classes where performance dropped."""
        current = self.compute_metrics()
        regressions = []
 
        for label in self.label_names:
            baseline_f1 = baseline_metrics["per_class_f1"].get(
                label, 0
            )
            current_f1 = current["per_class_f1"].get(label, 0)
 
            if baseline_f1 - current_f1 > threshold:
                regressions.append(
                    f"{label}: {baseline_f1:.3f} → {current_f1:.3f}"
                )
 
        return regressions

Deployment und Modell-Versionierung

Feinabgestimmte Modelle brauchen die gleiche Rigorosität wie Produktionssoftware: Versionierung, Rollback-Fähigkeit und A/B-Test-Infrastruktur.

tstypescript
interface ModelVersion {
  id: string;
  baseModel: string;
  trainingDate: string;
  datasetVersion: string;
  metrics: {
    macroF1: number;
    weightedF1: number;
    evaluationSet: string;
  };
  status: "training" | "evaluating" | "canary" | "production" | "retired";
}
 
class ModelRegistry {
  private versions: Map<string, ModelVersion> = new Map();
 
  async promote(
    modelId: string,
    targetStatus: ModelVersion["status"]
  ): Promise<void> {
    const model = this.versions.get(modelId);
    if (!model) throw new Error(`Model ${modelId} not found`);
 
    // Enforce promotion order
    const validTransitions: Record<string, string[]> = {
      training: ["evaluating"],
      evaluating: ["canary", "retired"],
      canary: ["production", "retired"],
      production: ["retired"],
    };
 
    const allowed = validTransitions[model.status] ?? [];
    if (!allowed.includes(targetStatus)) {
      throw new Error(
        `Cannot transition from ${model.status} to ${targetStatus}`
      );
    }
 
    // If promoting to production, retire current production model
    if (targetStatus === "production") {
      for (const [id, ver] of this.versions) {
        if (ver.status === "production" && id !== modelId) {
          ver.status = "retired";
        }
      }
    }
 
    model.status = targetStatus;
  }
}

Kernpunkte

Fine-Tuning verwandelt ein Allzweckmodell in einen Domänenspezialisten, aber der Prozess erfordert Disziplin. Bereinige deine Daten aggressiv: Deduplizierung, Qualitätsfilterung und Längenbeschränkungen verhindern, dass das Modell Rauschen auswendig lernt. Starte mit konservativen Hyperparametern: eine Lernrate von 2e-5, 3 Epochen und Cosinus-Scheduling bilden eine zuverlässige Basis. Nutze LoRA oder ähnliche parameter-effiziente Methoden, wenn vollständiges Fine-Tuning dein Compute-Budget übersteigt: das Training von weniger als 1% der Parameter führt oft zu vergleichbaren Ergebnissen. Evaluiere mit domänenspezifischen Metriken, nicht nur mit Loss-Kurven, und verfolge die Leistung pro Klasse, um Regressionen in unterrepräsentierten Kategorien zu erkennen. Behandle Modellversionen wie Software-Releases: versioniere sie, steuere Promotionen über Evaluationsstufen und halte Rollback-Fähigkeit vor. Ziel ist nicht das mächtigste Modell, sondern das zuverlässigste für deine spezifische Aufgabe.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX