Kurs
Die Zusammenarbeit von Unsloth mit NVIDIA zielt darauf ab, Fine-Tuning durch geringeren versteckten Trainings-Overhead zu beschleunigen. Statt nur auf größere GPUs oder kleinere Modelle zu setzen, adressieren die Verbesserungen Engpässe im Trainingsprozess selbst, etwa wiederholte Metadaten-Erstellung, Verzögerungen beim Nachladen von Aktivierungen und ineffizientes Token-Routing. Für dich bedeutet das schnelleres Training und einen reibungsloseren Fine-Tuning-Workflow auf unterstützten NVIDIA-GPUs.
In diesem Guide lernst du die neuen Performanceverbesserungen von Unsloth kennen und setzt Unsloths optimierten Fine-Tuning-Workflow bei einer praktischen Vision-Language-Aufgabe ein. Wir werden Qwen3.5 Vision 4B für medizinisches OCR feinabstimmen, sodass das Modell aus Bildern medizinischer Dokumente strukturierte Texte extrahiert – mit einem kleinen, medizinisch wirkenden Teil eines OCR-Datasets.
Wir verwenden:
- Qwen3.5 4B (Vision) als Basismodell
- 4-Bit QLoRA zur Senkung der VRAM-Nutzung
- LoRA-Adapter für effizientes Fine-Tuning
- Unsloth Gradient-Checkpointing zur Speichereinsparung während des Trainings
- Eine 300-Beispiele-Teilstichprobe eines medizinischen OCR-Datasets
- Bildvorverarbeitung mit fester Größe für stabileres Vision-Training
- Vorher-nachher-Evaluierung zum Vergleich von Basis- und feinabgestimmtem Modell
Unsloths NVIDIA-optimierten Fine-Tuning-Workflow nutzen
Bevor wir mit dem Fine-Tuning starten, lohnt sich ein Blick darauf, was die Unsloth-NVIDIA-Zusammenarbeit verbessert und wie das in diesem Guide zum Tragen kommt.
Unsloth berichtet, dass die Zusammenarbeit LLM-Training um etwa 25% schneller macht – ohne Genauigkeitsverlust – zusätzlich zu den bereits bestehenden 2–5x-Speedups beim Fine-Tuning. Die Zugewinne entstehen durch die Reduktion von verstecktem Overhead rund um den eigentlichen Trainingsprozess, nicht durch Änderungen am Lernziel des Modells. Heißt: Fine-Tuning wird schneller und effizienter, die Genauigkeit bleibt erhalten.

Quelle: How to Make LLM Training Faster with Unsloth and NVIDIA
Willst du mit generativer KI beginnen?
Lerne, wie du mit LLMs in Python direkt in deinem Browser arbeiten kannst

Verbesserte Trainingsleistung
Die Zusammenarbeit liefert mehrere Leistungsgewinne, unter anderem:
- 14,3% schneller pro Batch bei einem Qwen3-14B QLoRA SFT-Benchmark durch Caching von Packed-Sequence-Metadaten
- 8,4% Speedup bei 8B-Modellen, 6,7% bei 14B und 4,6% bei 32B dank doppelgepuffertem asynchronem Gradient-Checkpointing
- Rund 10–15% Speedups für GPT-OSS MoE-Training, mit 23% schnellerem Forward- und 13% schnellerem Backward-Pass im gezielten Routing-Pfad
Einige der größten Performancegewinne aus der Unsloth-NVIDIA-Kollaboration betreffen gepacktes Text-only-Training und Mixture-of-Experts-Modelle. Das nutzen wir hier nicht, denn unser Workflow fokussiert auf das Qwen3.5 Vision OCR-Fine-Tuning.
Für diesen Guide nutzen wir eine NVIDIA RTX 3090 GPU. Der Workflow setzt daher auf NVIDIA-Beschleunigung und Unsloths optimierten Fine-Tuning-Pfad. Wir vergleichen Unsloth nicht gegen einen anderen Trainer, daher ist dies kein unabhängiger Nachweis der gemeldeten Speedups. Stattdessen wenden wir Unsloths optimierten Workflow auf eine reale Vision-Language-Aufgabe an.
Gradient-Checkpointing
Für diesen Workflow ist Unsloths Gradient-Checkpointing die wichtigste Optimierung. Es reduziert den Speicherbedarf während des Trainings, weil nicht jede Aktivierung im GPU-Speicher vorgehalten werden muss. Das ist besonders bei Vision-Language-Fine-Tuning hilfreich, wo sowohl Bild- als auch Texteingaben verarbeitet werden.
1. Unsloth für schnelleres Fine-Tuning einrichten
Um diesen Guide auszuführen, brauchst du Zugriff auf eine NVIDIA-GPU. Du kannst eine über Plattformen wie RunPod, Vast.ai oder andere Cloud-GPU-Anbieter mieten. Ich habe zunächst RunPod ausprobiert, da es meist schnell und zuverlässig ist, aber zum Zeitpunkt der Tests gab es nur wenige RTX 3090-Optionen. Daher nutzte ich für diesen Workflow eine Vast.ai-RTX-3090-Maschine.
Einen Vergleich der Plattformen findest du in unserem Guide zu den besten Cloud-GPU-Anbietern.

Quelle: Vast.ai | Console
Nach dem Starten der Instanz habe ich Jupyter Notebook geöffnet und ein neues Notebook erstellt. Auf Vast.ai habe ich den verfügbaren Haupt-Kernel des Environments gewählt, um die benötigten Python-Pakete innerhalb der Notebook-Umgebung zu installieren, ohne Systemabhängigkeiten zu verändern.
Benötigte Pakete installieren
Zuerst installierst du die erforderlichen Pakete für Unsloth, PyTorch, Vision-Training, Datenladen und die Integration mit Hugging Face:
!pip install --upgrade \
"torch>=2.8.0" "triton>=3.4.0" \
numpy pillow torchvision bitsandbytes \
unsloth "unsloth_zoo>=2026.4.6" \
"datasets>=4.0.0" huggingface_hub hf_transfer pandas \
transformers==5.2.0 torchcodec timm
Diese Pakete orientieren sich am offiziellen Unsloth-Notebook-Setup und decken die Kernbibliotheken zum Laden von Qwen3.5 Vision, zur Aufbereitung von Bild-Text-Daten und zum Fine-Tuning mit Unsloth ab.
CUDA-Gerät konfigurieren
Als Nächstes konfigurieren wir das CUDA-Gerät und prüfen, ob die richtige NVIDIA-GPU verfügbar ist. Da dieser Guide eine RTX 3090 nutzt, prüft der Code, ob CUDA aktiviert ist, bestätigt die ausgewählte GPU, gibt CUDA- und PyTorch-Versionen aus und verifiziert, dass genug VRAM für dieses Experiment vorhanden ist.
import os
import platform
CUDA_DEVICE_INDEX = 0
TARGET_GPU_NAME = "3090"
# Must be set before CUDA / Unsloth are initialized. Restart the kernel if you change these.
os.environ["CUDA_VISIBLE_DEVICES"] = str(CUDA_DEVICE_INDEX)
# RunPod + Qwen3.5 Vision OCR can hit Torch Dynamo fullgraph recompile limits.
# This disables Unsloth's torch.compile path while keeping Unsloth model loading,
# LoRA, gradient checkpointing, collator, and 8-bit optimizer benefits.
os.environ["UNSLOTH_COMPILE_DISABLE"] = "1"
os.environ["TORCH_COMPILE_DISABLE"] = "1"
import torch
DEVICE = torch.device("cuda:0")
print("Python:", platform.python_version())
print("PyTorch:", torch.__version__)
print("CUDA available:", torch.cuda.is_available())
if not torch.cuda.is_available():
raise RuntimeError("CUDA is not available. Select a GPU instance before continuing.")
torch.cuda.set_device(0)
props = torch.cuda.get_device_properties(0)
gpu_name = torch.cuda.get_device_name(0)
total_gpu_memory_gb = props.total_memory / 1024**3
print("Selected device:", DEVICE)
print("GPU:", gpu_name)
print("CUDA version:", torch.version.cuda)
print("BF16 supported:", torch.cuda.is_bf16_supported())
print("Total GPU memory:", round(total_gpu_memory_gb, 2), "GB")
if TARGET_GPU_NAME not in gpu_name:
raise RuntimeError(f"Expected an RTX {TARGET_GPU_NAME}, but CUDA device 0 is: {gpu_name}")
if total_gpu_memory_gb < 20:
raise RuntimeError(f"Expected a 24 GB class 3090, but only found {total_gpu_memory_gb:.2f} GB VRAM.")
In meinem Setup lieferte die Umgebung folgende GPU-Konfiguration zurück:
Python: 3.12.13
PyTorch: 2.12.0+cu130
CUDA available: True
Selected device: cuda:0
GPU: NVIDIA GeForce RTX 3090
CUDA version: 13.0
BF16 supported: True
Total GPU memory: 23.56 GB
Das bestätigt, dass das Notebook auf einer NVIDIA GeForce RTX 3090 mit ausreichend VRAM für das Fine-Tuning-Experiment läuft.
Trainingseinstellungen und Prompts definieren
Nachdem die GPU verifiziert ist, definieren wir Modell, Dataset, Trainingseinstellungen, Ausgabeverzeichnisse, Bildgröße und OCR-Prompts.
MODEL_NAME = "unsloth/Qwen3.5-4B"
DATASET_NAME = "naazimsnh02/medocr-vision-dataset"
SAMPLE_COUNT = 300
EVAL_INDEX = 0
MAX_LENGTH = 4096
MAX_STEPS = 30
PER_DEVICE_BATCH_SIZE = 4
GRADIENT_ACCUMULATION_STEPS = 2
LEARNING_RATE = 2e-4
SEED = 3407
OUTPUT_DIR = "outputs/qwen35_vision_medical_ocr"
ADAPTER_DIR = "qwen35-vision-medical-ocr-lora"
# Medical document images vary heavily in size. Fixed-size canvases avoid
# repeated Torch Dynamo recompiles during vision training.
# 768x1024 is a practical portrait-page compromise for a 24 GB 3090 smoke test.
FIXED_IMAGE_SIZE = (768, 1024)
# Official Unsloth Qwen3.5 Vision notebook uses False here for 16-bit LoRA.
# Set True only if you hit VRAM limits.
LOAD_IN_4BIT = True
SYSTEM_PROMPT = "You are a medical OCR transcription engine. Return only the exact text visible in the medical document image."
INSTRUCTION = "Extract all readable text from this medical document exactly. Preserve structure when possible. Return only the OCR text, with no explanation, no diagnosis, no medical advice, and no reasoning."
Hier nutzen wir Qwen3.5 Vision 4B von Unsloth und das medizinische OCR-Vision-Dataset. Für diesen Guide wählen wir 300 Beispiele und trainieren 30 Schritte – leichtgewichtig, aber aussagekräftig genug, um die Anpassung an das Ziel-OCR-Format zu zeigen.
Die feste Bildgröße von 768×1024 hält die Bildeingaben im Training konsistent. Medizinische Dokumente variieren stark in Auflösung und Seitenverhältnis; das Umsetzen auf eine feste Leinwand macht den Workflow stabiler und reduziert Formfehler beim Vision-Language-Fine-Tuning.
2. Modell laden
Jetzt, da die Umgebung steht, laden wir das Qwen3.5 Vision 4B-Modell mit Unsloths FastVisionModel.
import unsloth
from unsloth import FastVisionModel
torch.cuda.set_device(0)
model, tokenizer = FastVisionModel.from_pretrained(
MODEL_NAME,
load_in_4bit=LOAD_IN_4BIT,
use_gradient_checkpointing="unsloth",
)
print("Loaded:", MODEL_NAME)
print("4-bit:", LOAD_IN_4BIT)
print("Model device:", next(model.parameters()).device)
Nach dem Laden bestätigt die Ausgabe, dass das richtige Modell geladen wurde, der 4-Bit-Modus aktiv ist und das Modell auf der GPU liegt:
Loaded: unsloth/Qwen3.5-4B
4-bit: True
Model device: cuda:0
Hier lädt FastVisionModel.from_pretrained() das Vision-Language-Modell und aktiviert Unsloths Optimierungen für schnelleres und speichereffizienteres Fine-Tuning. Außerdem aktivieren wir load_in_4bit, wodurch das Modell in 4-Bit-Präzision geladen wird – praktisch bei einer 24-GB-GPU wie der RTX 3090.
Zudem aktivieren wir Unsloth Gradient-Checkpointing mit use_gradient_checkpointing="unsloth"
Das reduziert den Speicherbedarf während des Trainings – besonders wichtig bei Vision-Language-Modellen, da sie Bild- und Texteingaben verarbeiten.
3. LoRA-Adapter hinzufügen
Als Nächstes fügen wir LoRA-Adapter zum Modell hinzu. LoRA ermöglicht es, nur einen kleineren Teil trainierbarer Parameter zu optimieren statt das komplette Modell. Das macht das Training schneller, speichereffizienter und einfacher auf einer einzelnen GPU.
model = FastVisionModel.get_peft_model(
model,
finetune_vision_layers=True,
finetune_language_layers=True,
finetune_attention_modules=True,
finetune_mlp_modules=True,
r=16,
lora_alpha=16,
lora_dropout=0,
bias="none",
random_state=SEED,
use_rslora=False,
loftq_config=None,
)
In diesem Guide werden die Adapter sowohl über die Vision- als auch die Sprachkomponenten gelegt. So lernt das Modell, medizinische Dokumentbilder zu lesen und den erwarteten strukturierten OCR-Text zu erzeugen. Danach ist das Modell bereit für das Training auf dem medizinischen OCR-Dataset.
4. Medizinisches OCR-Dataset laden
Nun laden wir das medizinische OCR-Dataset von Hugging Face und bereiten eine kleine Teilmenge für das Fine-Tuning vor.
from datasets import load_dataset
from PIL import Image
raw_dataset = load_dataset(DATASET_NAME, split="train")
MEDICAL_KEYWORDS = [
"doctor", "dr.", "clinic", "hospital", "patient", "medication",
"medications", "prescription", "signature", "department", "report",
"diagnosis", "lab", "laboratory", "blood", "hemoglobin", "mg", "dose",
"<s_ocr>",
]
Das Dataset enthält Dokumentbilder und den zugehörigen OCR-Text. Da wir nur medizinisch anmutende OCR-Beispiele möchten, filtern wir per einfacher Schlüsselwortsuche. Der Code sucht nach typischen Begriffen aus medizinischen Dokumenten, etwa Arzt, Klinik, Patient, Medikamente, Rezept, Diagnose und Dosierungsangaben.
def looks_medical(sample):
text = str(sample.get("text", "")).lower()
return any(keyword in text for keyword in MEDICAL_KEYWORDS)
medical_indices = []
for idx, sample in enumerate(raw_dataset):
if looks_medical(sample):
medical_indices.append(idx)
if len(medical_indices) >= SAMPLE_COUNT:
break
if not medical_indices:
raise RuntimeError("No medical-looking OCR samples found. Broaden MEDICAL_KEYWORDS or inspect the dataset text field.")
print(f"Selected {len(medical_indices)} medical-looking samples.")
So wählen wir schlank Beispiele aus, die für die medizinische OCR-Aufgabe relevant wirken. Für diesen Lauf nehmen wir 300 solcher Beispiele.
Als Nächstes normalisieren wir jedes Bild auf eine feste 768×1024-Leinwand. Da medizinische Dokumente stark in Größe und Seitenverhältnis variieren, sorgt dieser Schritt für konsistentere Trainingsdaten. Das Bild wird unter Beibehaltung des Seitenverhältnisses skaliert und auf einen weißen Hintergrund gesetzt.
def normalize_ocr_image(image, size=FIXED_IMAGE_SIZE):
image = image.convert("RGB")
target_w, target_h = size
scale = min(target_w / image.width, target_h / image.height)
new_w = max(1, int(image.width * scale))
new_h = max(1, int(image.height * scale))
resized = image.resize((new_w, new_h), Image.Resampling.LANCZOS)
canvas = Image.new("RGB", size, "white")
left = (target_w - new_w) // 2
top = (target_h - new_h) // 2
canvas.paste(resized, (left, top))
return canvas
Anstatt datasets.map zu verwenden, bauen wir eine einfache Python-Liste manuell auf. So vermeiden wir mögliche Hänger in manchen Cloud-Notebook-Umgebungen beim Umschreiben von PIL-Bildern.
dataset = []
for idx in medical_indices:
sample = raw_dataset[idx]
dataset.append(
{
"image": normalize_ocr_image(sample["image"]),
"text": sample["text"],
}
)
print("Examples:", len(dataset))
print("Columns:", list(dataset[0].keys()))
print("Fixed image size:", dataset[EVAL_INDEX]["image"].size)
print("Sample text:", dataset[EVAL_INDEX]["text"])
Nach der Vorverarbeitung enthält jedes Beispiel zwei Felder: das normalisierte Bild und den Ziel-OCR-Text.
Examples: 300
Columns: ['image', 'text']
Fixed image size: (768, 1024)
Sample text: <s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>
Wir können außerdem eines der skalierten Beispiele vorab ansehen:
dataset[EVAL_INDEX]["image"].resize((384, 512))
Die Vorschau zeigt ein medizinisch wirkendes Dokument mit Klinikdetails, Arztname, Patientendaten, Medikamenten und Unterschrift. Das bestätigt die Eignung des Datasets für die OCR-Feinabstimmung.

5. Beispiele in Vision-Konversationen umwandeln
Nachdem Dataset und Bildnormalisierung stehen, müssen wir jedes Beispiel in das von Qwen3.5 Vision erwartete Konversationsformat umwandeln.
Jedes Trainingsbeispiel sollte drei Teile enthalten:
- Eine Systemnachricht, die die Rolle des Modells als medizinische OCR-Transkriptionsengine definiert
- Eine Benutzernachricht mit Bild und OCR-Anweisung
- Eine Assistenznachricht mit der erwarteten OCR-Antwort
def build_ocr_messages(image=None, target_text=None, instruction=INSTRUCTION):
user_content = [
{"type": "image"},
{"type": "text", "text": instruction},
]
if image is not None:
user_content[0]["image"] = image
messages = [
{"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
{"role": "user", "content": user_content},
]
if target_text is not None:
messages.append(
{
"role": "assistant",
"content": [{"type": "text", "text": target_text}],
}
)
return messages
Die Helper-Funktion oben erzeugt die Nachrichtenstruktur für Training und Inferenz. Beim Training geben wir den Ziel-OCR-Text als Assistenzantwort mit. Bei der Inferenz liefern wir nur Bild und Anweisung und lassen das Modell den OCR-Text generieren.
Als Nächstes wandeln wir jedes Datensatzbeispiel in dieses Konversationsformat um:
def convert_to_conversation(sample):
return {
"messages": build_ocr_messages(
image=sample["image"],
target_text=sample["text"],
)
}
converted_dataset = [convert_to_conversation(sample) for sample in dataset]
converted_dataset[0]
Nach der Umwandlung enthält jedes Beispiel eine Nachrichtenliste. Das erste Beispiel umfasst den Systemprompt, das medizinische Dokumentbild, die OCR-Anweisung sowie die erwartete, strukturierte OCR-Transkription. So lernt das Modell, ein Bild plus Anweisung dem korrekten Text-Output zuzuordnen.
{'messages': [{'role': 'system',
'content': [{'type': 'text',
'text': 'You are a medical OCR transcription engine. Return only the exact text visible in the medical document image.'}]},
{'role': 'user',
'content': [{'type': 'image',
'image': <PIL.Image.Image image mode=RGB size=768x1024>},
{'type': 'text',
'text': 'Extract all readable text from this medical document exactly. Preserve structure when possible. Return only the OCR text, with no explanation, no diagnosis, no medical advice, and no reasoning.'}]},
{'role': 'assistant',
'content': [{'type': 'text',
'text': '<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>'}]}]}
6. Basismodell vor dem Fine-Tuning evaluieren
Vor dem Training testen wir das Basismodell an einem OCR-Beispiel. So erhalten wir einen Referenzpunkt für den Vergleich vor und nach dem Fine-Tuning.
Zuerst definieren wir eine Helper-Funktion, um das Chat-Template des Modells anzuwenden. Manche Tokenizer-Versionen unterstützen enable_thinking=False, andere nicht – daher enthält die Funktion ein Fallback, um die Kompatibilität zu wahren.
def render_ocr_chat_template(tokenizer, messages):
try:
return tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=False,
)
except TypeError:
return tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
Als Nächstes definieren wir die Generierungsfunktion. Sie baut den OCR-Prompt, übergibt Bild und Textanweisungen an den Tokenizer, generiert die Modellausgabe und dekodiert nur die neu erzeugten Tokens.
def generate_ocr_text(model, tokenizer, image, instruction=INSTRUCTION, max_new_tokens=512):
messages = build_ocr_messages(instruction=instruction)
input_text = render_ocr_chat_template(tokenizer, messages)
inputs = tokenizer(
images=image,
text=input_text,
add_special_tokens=False,
return_tensors="pt",
).to(DEVICE)
with torch.inference_mode():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
use_cache=True,
do_sample=False,
temperature=None,
top_p=None,
)
prompt_length = inputs["input_ids"].shape[-1]
generated_tokens = outputs[:, prompt_length:]
return tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]
Nun schalten wir das Modell in den Inferenzmodus und generieren OCR-Text für das erste Evaluationsbild:
FastVisionModel.for_inference(model)
eval_image = dataset[EVAL_INDEX]["image"]
base_output = generate_ocr_text(model, tokenizer, eval_image)
print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nBase model output:")
print(base_output)
Die Basismodell-Ausgabe ist lesbar, folgt aber nicht exakt der Zielstruktur:
Target:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>
Base model output:
Meadowview Health
45 Oak Ave.
Prescribed by: Dr. A. Smith
Date: 2024-12-16
Patient: John Doe, Age: 35
Hydrochlorothiazide 25 mg - Before meals
Signature: Dr. A. Smith
Das ist ein guter Startpunkt. Das Basismodell liest bereits viel aus, gibt den Text aber in einem eher natürlichen OCR-Stil aus statt im strukturierten Datensatzformat. Fine-Tuning sollte die Ausgabe an das Zielformat anpassen und konsistenter machen.
7. Modell trainieren
Da das Dataset nun im richtigen Vision-Konversationsformat vorliegt, können wir das Modell mit dem SFTTrainer aus TRL und Unsloths Vision-Data-Collator trainieren.
from unsloth.trainer import UnslothVisionDataCollator
from trl import SFTTrainer, SFTConfig
FastVisionModel.for_training(model)
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
data_collator=UnslothVisionDataCollator(model, tokenizer),
train_dataset=converted_dataset,
args=SFTConfig(
per_device_train_batch_size=PER_DEVICE_BATCH_SIZE,
gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
warmup_steps=5,
max_steps=MAX_STEPS,
learning_rate=LEARNING_RATE,
logging_steps=1,
optim="adamw_8bit",
weight_decay=0.001,
lr_scheduler_type="linear",
seed=SEED,
output_dir=OUTPUT_DIR,
report_to="none",
remove_unused_columns=False,
dataset_text_field="",
dataset_kwargs={"skip_prepare_dataset": True},
max_length=MAX_LENGTH,
),
)
trainer_stats = trainer.train()
Zuerst schalten wir das Modell mit FastVisionModel.for_training(model) in den Trainingsmodus. Danach erstellen wir den Trainer mit dem konvertierten OCR-Datensatz.
Wichtig ist hier der UnslothVisionDataCollator. Da es sich um eine Vision-Language-Aufgabe handelt, muss der Trainer sowohl die medizinischen Dokumentbilder als auch den Ziel-OCR-Text korrekt verarbeiten. Der Collator bereitet diese multimodalen Beispiele für das supervised Fine-Tuning auf.
Für diesen Guide trainieren wir 30 Schritte mit einer per-Device-Batchsize von 4 und Gradient Accumulation von 2 – effektiv Batchgröße 8. So bleibt der Lauf leichtgewichtig und zeigt trotzdem, wie sich das Modell dem strukturierten OCR-Format annähert.

Während des Trainings druckt Unsloth nützliche Infos zum Setup, darunter Anzahl Beispiele, Schritte und Batches, Zahl trainierbarer Parameter und Speichersparfunktionen. In diesem Lauf meldet Unsloth, dass Double Buffering für den Backward-Pass aktiviert ist – das reduziert Wartezeiten beim Gradient-Checkpointing.
8. Feinabgestimmtes Modell evaluieren
Nach dem Training schalten wir zurück in den Inferenzmodus und generieren OCR-Text für dasselbe Evaluationsbild wie vor dem Fine-Tuning.
FastVisionModel.for_inference(model)
fine_tuned_output = generate_ocr_text(model, tokenizer, eval_image)
print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nBase model output:")
print(base_output)
print("\nFine-tuned output:")
print(fine_tuned_output)
Nach dem Fine-Tuning liegt die Ausgabe deutlich näher an der Zielstruktur des Datasets:
Target:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>
Base model output:
Meadowview Health
45 Oak Ave.
Prescribed by: Dr. A. Smith
Date: 2024-12-16
Patient: John Doe, Age: 35
Hydrochlorothiazide 25 mg - Before meals
Signature: Dr. A. Smith
Fine-tuned output:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>
Das zeigt: Das feinabgestimmte Modell hat das erwartete OCR-Antwortformat gelernt. Das Basismodell konnte den Text weitgehend lesen, aber erst das Fine-Tuning hat die Ausgabe konsistent ans Strukturformat des Trainingsdatensatzes angepasst.
Wir testen das Modell außerdem an einem weiteren Beispiel aus dem Dataset:
EVAL_INDEX_2 = 35
eval_image_2 = dataset[EVAL_INDEX_2]["image"]
fine_tuned_output = generate_ocr_text(model, tokenizer, eval_image_2)
print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nFine-tuned output:")
print(fine_tuned_output)
Im zweiten Beispiel hält das Modell die Struktur ein, macht aber einen kleinen OCR-Fehler und erzeugt Amoxicillin statt Amlodipin:
Target:
<s_ocr> doctor_name: Dr. C. Rossi clinic_name: Riverside Clinic clinic_address: 45 Oak Ave. patient_name: Wei Li patient_age: 70 date: 2024-12-16 medications: - Acetaminophen 20 mg - Take twice daily - Amlodipine 20 mg - After meals signature: Dr. C. Rossi </s>
Fine-tuned output:
<s_ocr> doctor_name: Dr. C. Rossi clinic_name: Riverside Clinic clinic_address: 45 Oak Ave. patient_name: Wei Li patient_age: 70 date: 2024-12-16 medications: - Acetaminophen 20 mg - Take twice daily - Amoxicillin 20 mg - After meals signature: Dr. C. Rossi </s>
Das erinnert daran: Das Format sitzt besser, aber die OCR-Genauigkeit hängt weiter von Datenqualität, Bildschärfe, Trainingsumfang und der Zahl der Fine-Tuning-Schritte ab. Für ein produktives OCR-System würdest du auf einem größeren, vielfältigeren Datensatz trainieren und die Genauigkeit breit evaluieren.
9. Den feinabgestimmten Adapter speichern
Nach dem Training speichern wir den LoRA-Adapter und den Tokenizer lokal.
model.save_pretrained(ADAPTER_DIR)
tokenizer.save_pretrained(ADAPTER_DIR)
print("Saved adapter to:", ADAPTER_DIR)
Die Ausgabe bestätigt das Speichern des Adapters:
Saved adapter to: qwen35-vision-medical-ocr-lora
Gespeichert werden nur die feinabgestimmten Adaptergewichte, nicht eine vollständige Kopie des Basismodells. Später kannst du das Basis-Qwen3.5-4B-Modell erneut laden und diesen Adapter anwenden, um das OCR-Verhalten wiederzuverwenden. Das macht das Artefakt leichtgewichtig und einfacher zu speichern, zu teilen oder zu deployen.
Abschließende Gedanken
Der Trainingslauf war leichtgewichtig und praxistauglich auf einer einzelnen NVIDIA RTX 3090. Obwohl Vision-Language-Fine-Tuning normalerweise speicherhungrig ist, lag die VRAM-Nutzung deutlich unter Erwartung: maximal etwa 14 GB, durchschnittlich näher bei 9 GB – beeindruckend für das Fine-Tuning eines Qwen3.5-Vision-Modells.
Auch die Adaption ging schnell. Schon nach wenigen Trainingsschritten näherte sich die Ausgabe stark der Zielstruktur an. Das Basismodell konnte das Dokument bereits lesen, aber nach dem Fine-Tuning hielt es das Datensatzformat deutlich konsistenter ein.
Allerdings war die Einrichtung nicht völlig reibungslos. Die Installation von Unsloth erforderte viel Trial-and-Error. Die korrekte Konfiguration kann knifflig sein – besonders bei unterschiedlichen lokalen oder virtuellen Umgebungen, CUDA-Versionen und Cloud-GPU-Anbietern.
In manchen Fällen können CUDA-Kompatibilitätsprobleme die Umgebung zerschießen, und das Debuggen dauert länger als erwartet. Selbst mit einem Unsloth-Docker-Image auf einer Cloud-GPU-Plattform kann es zeitaufwendig sein, wenn das Environment nicht out of the box sauber läuft.
Ein weiterer wichtiger Punkt: Das Modell-Template ist entscheidend. Wenn das Dataset nicht in das richtige Chat- oder Vision-Konversationsformat konvertiert wird, lernt das Modell nicht richtig. Für Qwen3.5 Vision ist die korrekte Bild-Text-Nachrichtenstruktur essenziell. Ohne das passende Template kann das Training zwar laufen, aber das Modell passt sich unter Umständen nicht an die Aufgabe an.
Unterm Strich ist Unsloth eine starke Option für alle mit begrenztem GPU-Zugang, die Modelle effizient auf lokalen Maschinen oder gemieteten GPUs feinabstimmen möchten. Es senkt den Speicherbedarf, macht kleinere Hardware nutzbarer und kann Experimente beschleunigen. Wer jedoch regelmäßig Modelle feinabstimmt und trainiert, könnte die Setup-Komplexität frustrierend finden. Standard-Training auf Basis von Transformers ist oft stabiler, einfacher zu installieren und leichter reproduzierbar.
Wenn dich vor allem die Installation abschreckt, empfehle ich unseren Guide zu Unsloth Studio. Er zeigt, wie du Qwen3.5-9B ohne manuelles Environment-Setup in Unsloths lokaler Weboberfläche feinabstimmst.
Als zertifizierter Data Scientist ist es meine Leidenschaft, modernste Technologien zu nutzen, um innovative Machine Learning-Anwendungen zu entwickeln. Mit meinem fundierten Hintergrund in den Bereichen Spracherkennung, Datenanalyse und Reporting, MLOps, KI und NLP habe ich meine Fähigkeiten bei der Entwicklung intelligenter Systeme verfeinert, die wirklich etwas bewirken können. Neben meinem technischen Fachwissen bin ich auch ein geschickter Kommunikator mit dem Talent, komplexe Konzepte in eine klare und prägnante Sprache zu fassen. Das hat dazu geführt, dass ich ein gefragter Blogger zum Thema Datenwissenschaft geworden bin und meine Erkenntnisse und Erfahrungen mit einer wachsenden Gemeinschaft von Datenexperten teile. Zurzeit konzentriere ich mich auf die Erstellung und Bearbeitung von Inhalten und arbeite mit großen Sprachmodellen, um aussagekräftige und ansprechende Inhalte zu entwickeln, die sowohl Unternehmen als auch Privatpersonen helfen, das Beste aus ihren Daten zu machen.
