Przejdź do głównej treści

Gradient Checkpointing: wydajne dostrajanie z Unsloth i NVIDIA

Dowiedz się, jak gradient checkpointing zoptymalizowany przez NVIDIA w Unsloth i QLoRA zmniejszają użycie VRAM podczas dostrajania Qwen3.5 4B do medycznego OCR dokumentów.
Zaktualizowano 14 sie 2026  · 12 min Czytać

Eksploruj z AI

ChatGPTClaudePerplexity

Współpraca Unsloth z NVIDIA koncentruje się na przyspieszeniu dostrajania poprzez redukcję ukrytych narzutów treningowych. Zamiast polegać wyłącznie na większych GPU lub mniejszych modelach, usprawnienia celują w wąskie gardła wewnątrz procesu treningowego, takie jak powtarzalna konstrukcja metadanych, opóźnienia przy ponownym ładowaniu aktywacji czy nieefektywne trasowanie tokenów. Dla użytkowników oznacza to szybsze trenowanie i płynniejszy workflow dostrajania na wspieranych GPU NVIDIA.

W tym przewodniku poznamy te nowe ulepszenia wydajności w Unslotha i zastosujemy zoptymalizowany przez Unsloth workflow dostrajania do praktycznego zadania łączącego wizję i język. Dostroimy Qwen3.5 Vision 4B do medycznego OCR, gdzie model uczy się wyodrębniać ustrukturyzowany tekst z obrazów dokumentów medycznych, korzystając z małego, „medycznie wyglądającego” podzbioru datasetu OCR.

Użyjemy:

  • Qwen3.5 4B (Vision) jako modelu bazowego
  • 4-bitowego QLoRA w celu zmniejszenia użycia VRAM
  • Adapterów LoRA do wydajnego dostrajania
  • Gradient checkpointingu Unsloth w celu oszczędzania pamięci podczas treningu
  • Podzbioru 300 próbek z medycznego datasetu OCR
  • Przetwarzania obrazów do stałego rozmiaru dla płynniejszego treningu części wizualnej
  • Ewaluacji przed i po, aby porównać wyniki modelu bazowego i dostrojonego

Korzystanie z workflow dostrajania Unsloth zoptymalizowanego przez NVIDIA

Zanim zaczniemy dostrajanie, warto zrozumieć, co współpraca Unsloth z NVIDIA ulepsza i jak łączy się z tym przewodnikiem.

Unsloth podaje, że współpraca z NVIDIA przyspiesza trening LLM o około 25% bez utraty dokładności, ponad dotychczasowe przyspieszenia 2–5x przy dostrajaniu. Te zyski wynikają z redukcji ukrytych narzutów wokół głównego procesu treningowego, a nie ze zmiany celu uczenia modelu. Innymi słowy, celem jest przyspieszenie i zwiększenie efektywności dostrajania przy zachowaniu niezmienionej dokładności.

Source: How to Make LLM Training Faster with Unsloth and NVIDIA

Źródło: How to Make LLM Training Faster with Unsloth and NVIDIA 

Lepsza wydajność treningu

Współpraca raportuje kilka usprawnień wydajności, m.in.:

  • 14,3% szybciej na batch na benchmarku Qwen3-14B QLoRA SFT dzięki cache’owaniu metadanych dla pakowanych sekwencji
  • Przyspieszenie o 8,4% dla modeli 8B, 6,7% dla 14B i 4,6% dla 32B dzięki podwójnie buforowanemu asynchronicznemu gradient checkpointingowi
  • Około 10–15% przyspieszenia treningu GPT-OSS MoE, z 23% szybszym forward i 13% szybszym backward na ścieżce targetowanego trasowania

Największe zyski z współpracy Unsloth i NVIDIA dotyczą pakowanego treningu wyłącznie tekstowego oraz modeli Mixture-of-Experts. Nie używamy ich w tym przewodniku, bo nasz workflow skupia się na dostrajaniu OCR w Qwen3.5 Vision. 

W tym przewodniku korzystamy z GPU NVIDIA RTX 3090, więc workflow opiera się na akceleracji GPU NVIDIA i zoptymalizowanej ścieżce dostrajania Unsloth. Nie porównujemy Unsloth z innym trenerem, więc tego przewodnika nie należy traktować jako niezależnego potwierdzenia raportowanych przyspieszeń. Zamiast tego stosujemy zoptymalizowany workflow Unsloth do realnego zadania vision-language.

Gradient checkpointing

Dla tego workflow najistotniejszą optymalizacją jest gradient checkpointing Unsloth. Pomaga on zmniejszyć użycie pamięci podczas treningu, unikając potrzeby przechowywania każdej aktywacji w pamięci GPU. Jest to szczególnie przydatne przy dostrajaniu z wizją i językiem, gdzie model musi przetwarzać zarówno obrazy, jak i tekst.

1. Przygotowanie Unsloth do szybszego dostrajania

Aby uruchomić ten przewodnik, potrzebujesz dostępu do GPU NVIDIA. Możesz wynająć taki sprzęt na platformach takich jak RunPod, Vast.ai lub inny dostawca GPU w chmurze. Początkowo próbowałem użyć RunPod, bo zwykle jest szybki i niezawodny, ale dostępność opcji RTX 3090 była w tym czasie ograniczona. Dlatego użyłem maszyny Vast.ai z GPU RTX 3090 dla tego workflow. 

Porównanie różnych platform znajdziesz w naszym poradniku o najlepszych dostawcach GPU w chmurze.

Vast.ai RTX 3090 instance

Źródło:  Vast.ai | Console 

Po uruchomieniu instancji otworzyłem Jupyter Notebook i utworzyłem nowy notatnik. Na Vast.ai wybrałem dostępne główne jądro środowiska, aby zainstalować wymagane pakiety Pythona w środowisku notatnika bez wpływu na zależności systemowe. 

Instalowanie wymaganych pakietów

Najpierw zainstaluj wymagane pakiety dla Unsloth, PyTorch, treningu modeli wizji, ładowania datasetu i integracji z 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

Pakiety te bazują na oficjalnym notatniku z konfiguracją Unsloth i obejmują główne biblioteki potrzebne do wczytania Qwen3.5 Vision, przygotowania danych obraz-tekst i dostrojenia modelu za pomocą Unsloth. 

Konfiguracja urządzenia CUDA

Następnie konfigurujemy urządzenie CUDA i weryfikujemy dostępność poprawnego GPU NVIDIA. Ponieważ w tym przewodniku używamy RTX 3090, kod sprawdza, czy CUDA jest włączone, potwierdza wybrany GPU, wypisuje wersje CUDA i PyTorch oraz weryfikuje, czy maszyna ma wystarczająco VRAM na ten eksperyment. 

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.")

W mojej konfiguracji środowisko zwróciło następujące ustawienia GPU:

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

To potwierdza, że notatnik działa na NVIDIA GeForce RTX 3090 z wystarczającą ilością VRAM na eksperyment z dostrajaniem.

Definiowanie ustawień treningu i promptów

Po weryfikacji GPU definiujemy model, dataset, ustawienia treningowe, katalogi wyjściowe, rozmiar obrazu i prompty do OCR.

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."

Tutaj używamy Qwen3.5 Vision 4B od Unsloth i medycznego datasetu OCR dla wizji. Do tego przewodnika wybieramy 300 próbek i trenujemy przez 30 kroków, co utrzymuje lekkość uruchomienia, a jednocześnie pokazuje, jak model dostosowuje się do docelowego formatu OCR.

Stały rozmiar obrazu 768×1024 pomaga utrzymać spójność wejść obrazowych podczas treningu. Dokumenty medyczne mogą się znacznie różnić rozdzielczością i proporcjami, więc przeskalowanie ich do stałego płótna wygładza workflow i redukuje problemy z kształtami podczas dostrajania vision-language.

2. Wczytywanie modelu

Skoro środowisko jest gotowe, możemy wczytać model Qwen3.5 Vision 4B za pomocą FastVisionModel z Unsloth

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)

Po wczytaniu modelu wynik potwierdza, że załadowano poprawny model, tryb 4-bit jest włączony, a model znajduje się na GPU: 

Loaded: unsloth/Qwen3.5-4B
4-bit: True
Model device: cuda:0

Tutaj FastVisionModel.from_pretrained() wczytuje model vision-language i stosuje optymalizacje Unsloth dla szybszego i bardziej oszczędnego pamięciowo dostrajania. Włączamy też load_in_4bit, co zmniejsza użycie VRAM dzięki 4-bitowej precyzji. To przydatne przy pracy z 24 GB GPU, takim jak RTX 3090.

Włączamy też gradient checkpointing Unsloth poprzez use_gradient_checkpointing="unsloth"

To pomaga ograniczyć użycie pamięci podczas treningu, co jest szczególnie ważne dla modeli vision-language, bo przetwarzają jednocześnie obrazy i tekst. 

3. Dodanie adapterów LoRA

Następnie dodajemy adaptery LoRA do modelu. LoRA pozwala dostrajać mniejszy zestaw trenowalnych parametrów zamiast aktualizować cały model. Dzięki temu trening jest szybszy, bardziej oszczędny pamięciowo i łatwiejszy do uruchomienia na pojedynczym 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,
)

W tym przewodniku adaptery są dodane zarówno do części wizualnej, jak i językowej modelu. Pomaga to modelowi nauczyć się odczytywać obrazy dokumentów medycznych i generować oczekiwany ustrukturyzowany tekst OCR. Po tym kroku model jest gotowy do treningu na medycznym datasetcie OCR. 

4. Wczytanie medycznego datasetu OCR

Teraz wczytujemy medyczny dataset OCR z Hugging Face i przygotowujemy mały podzbiór do dostrajania.

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>",
]

Dataset zawiera obrazy dokumentów i odpowiadający im tekst OCR. Ponieważ w tym przewodniku chcemy wyłącznie przykłady w stylu medycznym, filtrujemy dataset prostym podejściem opartym na słowach kluczowych. Kod szuka terminów często występujących w dokumentach medycznych, takich jak doctor, clinic, patient, medication, prescription, diagnosis oraz słowa związane z dawkowaniem. 

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.")

To daje lekki sposób wyboru przykładów, które wyglądają na istotne dla zadania medycznego OCR. W tym uruchomieniu wybieramy 300 próbek o „medycznym” charakterze.

Następnie normalizujemy każdy obraz do stałego płótna 768×1024. Obrazy dokumentów medycznych mogą mieć różne rozmiary i proporcje, więc ten krok pomaga ujednolicić dane treningowe. Obraz jest przeskalowywany z zachowaniem oryginalnych proporcji, a następnie umieszczany na białym tle. 

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

Zamiast używać datasets.map, ręcznie budujemy prostą listę w Pythonie. To pozwala uniknąć potencjalnych zawieszeń w niektórych środowiskach notebooków w chmurze przy przepisywaniu obrazów PIL. 

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"])

Po wstępnym przetwarzaniu każdy przykład zawiera dwa pola: znormalizowany obraz i docelowy tekst OCR. 

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>

Możemy też podejrzeć jeden z przeskalowanych przykładów:

dataset[EVAL_INDEX]["image"].resize((384, 512))

Podgląd pokazuje dokument w stylu medycznym ze szczegółami przychodni, nazwiskiem lekarza, informacjami o pacjencie, lekami i podpisem. To potwierdza, że dataset nadaje się do zadania dostrajania OCR. 

Doctors prescription.

5. Konwersja próbek do rozmów wizualnych

Skoro dataset jest wczytany, a obrazy znormalizowane, musimy przekonwertować każdy przykład do formatu rozmowy oczekiwanego przez Qwen3.5 Vision.

Każda próbka treningowa powinna zawierać trzy części:

  • Wiadomość systemową, która definiuje rolę modelu jako silnika transkrypcji medycznego OCR
  • Wiadomość użytkownika zawierającą obraz i instrukcję OCR
  • Wiadomość asystenta zawierającą oczekiwany wynik OCR
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

Powyższa funkcja pomocnicza tworzy strukturę wiadomości zarówno dla treningu, jak i inferencji. Podczas treningu dołączamy docelowy tekst OCR jako odpowiedź asystenta. W inferencji dostarczamy tylko obraz i instrukcję, a następnie prosimy model o wygenerowanie tekstu OCR.

Następnie konwertujemy każdą próbkę datasetu do tego formatu rozmowy:

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]

Po konwersji każda próbka zawiera listę wiadomości. Pierwszy przykład obejmuje prompt systemowy, obraz dokumentu medycznego, instrukcję OCR oraz oczekiwaną ustrukturyzowaną transkrypcję OCR. Ten format pozwala modelowi nauczyć się mapować obraz i instrukcję na poprawny wynik tekstowy. 

{'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. Ewaluacja modelu bazowego przed dostrajaniem

Przed treningiem warto przetestować model bazowy na jednym przykładzie OCR. Daje nam to punkt odniesienia, aby porównać wyniki modelu przed i po dostrajaniu.

Najpierw definiujemy funkcję pomocniczą do zastosowania szablonu czatu modelu. Niektóre wersje tokenizerów wspierają enable_thinking=False, a inne nie, więc funkcja zawiera mechanizm awaryjny, by zachować kompatybilność.

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,
        )

Następnie definiujemy funkcję generującą. Buduje ona prompt OCR, przekazuje do tokenizera zarówno obraz, jak i instrukcje tekstowe, generuje wynik modelu i dekoduje tylko nowo wygenerowane tokeny. 

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]

Teraz przełączamy model w tryb inferencji i generujemy tekst OCR dla pierwszego obrazu ewaluacyjnego:

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)

Wynik modelu bazowego jest czytelny, ale nie podąża dokładnie za docelową strukturą:

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

To użyteczny punkt startowy. Model bazowy potrafi już odczytać znaczną część dokumentu, ale wypluwa tekst w naturalnym stylu OCR zamiast w ustrukturyzowanym formacie z datasetu. Dostrajanie powinno pomóc zestroić model z docelowym formatem i zwiększyć spójność odpowiedzi.

7. Trenowanie modelu

Skoro dataset jest w poprawnym formacie rozmów wizualnych, możemy trenować model używając SFTTrainer z TRL wraz z wizualnym kolatorem danych Unsloth. 

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()

Najpierw przełączamy model w tryb treningu poprzez FastVisionModel.for_training(model). Następnie tworzymy trenera, korzystając ze skonwertowanego datasetu OCR.

Kluczowy jest tutaj UnslothVisionDataCollator. Ponieważ to zadanie łączy wizję i język, trener musi poprawnie obsłużyć zarówno obrazy dokumentów medycznych, jak i docelowy tekst OCR. Kolator przygotowuje te multimodalne przykłady do przekazania modelowi podczas nadzorowanego dostrajania.

W tym przewodniku trenujemy przez 30 kroków z batch size 4 na urządzenie i akumulacją gradientów 2, co daje efektywny batch 8. To utrzymuje lekkość uruchomienia, a jednocześnie pokazuje, jak model zaczyna dostosowywać się do ustrukturyzowanego formatu OCR.

Fine-tuning the Qwen 3.5 4b Vision language model

Podczas treningu Unsloth wypisuje przydatne informacje o konfiguracji, w tym liczbę przykładów, kroków i batchy, liczbę trenowalnych parametrów oraz funkcje oszczędzania pamięci. W tym uruchomieniu Unsloth raportuje, że dla passu wstecznego włączono double buffering, co pomaga skrócić czas oczekiwania podczas gradient checkpointingu.

8. Ewaluacja dostrojonego modelu

Po treningu przełączamy model z powrotem w tryb inferencji i generujemy tekst OCR dla tego samego obrazu ewaluacyjnego, co przed dostrajaniem.

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)

Po dostrojeniu wynik modelu jest znacznie bliższy docelowej strukturze z datasetu:

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>

To pokazuje, że dostrojony model nauczył się oczekiwanego formatu odpowiedzi OCR. Model bazowy potrafił już wyodrębnić większość widocznego tekstu, ale dostrajanie pomogło zestroić wyjście z ustrukturyzowanym formatem używanym w danych treningowych.

Możemy również przetestować model na innym przykładzie z datasetu:

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)

W tym drugim przykładzie model zachowuje oczekiwaną strukturę, ale popełnia drobny błąd OCR, generując Amoxicillin zamiast Amlodipine:

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>

To przydatne przypomnienie, że model poprawia się w dopasowaniu formatu, ale dokładność OCR wciąż zależy od jakości danych, czytelności obrazu, rozmiaru treningu i liczby kroków dostrajania. W produkcyjnym systemie OCR trenowałbyś na większym, bardziej zróżnicowanym datasetcie i oceniał dokładność na szerokim spektrum przykładów.

9. Zapisywanie dostrojonego adaptera

Po zakończeniu treningu zapisujemy lokalnie adapter LoRA i tokenizer.

model.save_pretrained(ADAPTER_DIR)
tokenizer.save_pretrained(ADAPTER_DIR)

print("Saved adapter to:", ADAPTER_DIR)

Wynik potwierdza, że adapter został zapisany:

Saved adapter to: qwen35-vision-medical-ocr-lora

To zapisuje jedynie wagi dostrojonego adaptera, a nie pełną kopię modelu bazowego. Później możesz ponownie wczytać bazowy model Qwen3.5-4B i zastosować ten adapter, by odtworzyć dostrojone zachowanie OCR. Dzięki temu zapisany model jest lekki i łatwiejszy do przechowywania, udostępniania lub wdrażania.

Podsumowanie

Proces treningu był lekki i praktyczny na pojedynczym NVIDIA RTX 3090. Mimo że dostrajanie vision-language zwykle jest pamięciożerne, zużycie VRAM było znacznie mniejsze niż oczekiwano. Maksymalne użycie VRAM wyniosło około 14 GB, a średnie było bliżej 9 GB, co jest imponujące jak na dostrajanie modelu Qwen3.5 Vision.

Model też szybko się adaptował. Już po kilku krokach treningu wynik stał się dużo bliższy docelowej strukturze OCR. Model bazowy potrafił czytać dokument, ale po dostrojeniu konsekwentniej podążał za formatem datasetu. 

Niemniej doświadczenie z konfiguracją nie było idealne. Instalacja Unsloth wymagała wielu prób i błędów. Może być trudna do poprawnego skonfigurowania, zwłaszcza przy pracy w różnych środowiskach lokalnych, wirtualnych, wersjach CUDA i u dostawców GPU w chmurze. 

W niektórych przypadkach problemy ze zgodnością CUDA mogą zepsuć środowisko, a debugowanie może zająć więcej czasu niż oczekiwano. Nawet start od obrazu Dockera Unsloth na platformie GPU w chmurze potrafi być czasochłonny, jeśli środowisko nie działa „z pudełka”.

Kolejna ważna lekcja to znaczenie szablonu modelu. Jeśli dataset nie zostanie przekonwertowany do odpowiedniego formatu czatu lub rozmowy wizualnej, model może nie uczyć się poprawnie. W przypadku Qwen3.5 Vision kluczowe jest użycie właściwej struktury wiadomości obraz-tekst. Bez odpowiedniego szablonu trening może się odbywać, ale model faktycznie nie dostosuje się do zadania.

Ogólnie rzecz biorąc, Unsloth to mocna opcja dla użytkowników z ograniczonym dostępem do GPU, którzy chcą efektywnie dostrajać modele na maszynach lokalnych lub wynajmowanych GPU. Zmniejsza użycie pamięci, lepiej wykorzystuje mniejszy sprzęt i może przyspieszyć eksperymentowanie. Jednak dla osób, które regularnie dostrajają i trenują modele, złożoność konfiguracji bywa frustrująca. Standardowy trening oparty na Transformers bywa stabilniejszy, łatwiejszy w instalacji i prostszy do odtworzenia w różnych środowiskach.

Jeśli to tarcia przy instalacji najbardziej cię zniechęcają, polecam lekturę naszego przewodnika po Unsloth Studio, który pokazuje, jak dostroić Qwen3.5-9B bez ręcznej konfiguracji środowiska, w lokalnym interfejsie webowym Unsloth.

Tematy

Najlepsze kursy AI

course

Wprowadzenie do uczenia głębokiego z PyTorch

4 godz.
88.5K
Dowiedz się, jak zbudować swoją pierwszą sieć neuronową, dostosować hiperparametry i rozwiązywać problemy klasyfikacji oraz regresji w PyTorch.
Zobacz szczegółyRight Arrow
Rozpocznij Kurs
Zobacz więcejRight Arrow