Course
Сотрудничество Unsloth с NVIDIA направлено на ускорение дообучения за счёт сокращения скрытых накладных расходов обучения. Вместо того чтобы полагаться только на более мощные GPU или меньшие модели, улучшения нацелены на узкие места внутри процесса обучения, такие как повторная сборка метаданных, задержки при повторной загрузке активаций и неэффективная маршрутизация токенов. Для пользователей это означает более быстрое обучение и более плавный рабочий процесс дообучения на поддерживаемых GPU NVIDIA.
В этом руководстве мы разберём новые улучшения производительности Unsloth и применим оптимизированный Unsloth’s optимизированный конвейер дообучения к практической задаче «визуализация + язык». Мы дообучим Qwen3.5 Vision 4B для медицинского OCR: модель научится извлекать структурированный текст из изображений медицинских документов, используя небольшой поднабор датасета OCR с «медицинским» видом.
Мы будем использовать:
- Qwen3.5 4B (Vision) в качестве базовой модели
- 4-битную QLoRA для снижения использования VRAM
- Адаптеры LoRA для эффективного дообучения
- Чекпоинтинг градиентов Unsloth для экономии памяти во время обучения
- Поднабор из 300 примеров из медицинского датасета OCR
- Предобработку изображений до фиксированного размера для более стабильного обучения по изображениям
- Оценку «до и после», чтобы сравнить выходы базовой и дообученной моделей
Использование оптимизированного NVIDIA конвейера дообучения Unsloth
Перед началом дообучения полезно понять, что именно даёт сотрудничество Unsloth с NVIDIA и как это связано с данным руководством.
Unsloth сообщает, что сотрудничество с NVIDIA делает обучение LLM примерно на 25% быстрее без потери точности, сверх уже имеющихся ускорений дообучения в 2–5 раз. Эти преимущества достигаются за счёт сокращения скрытых накладных расходов вокруг основного процесса обучения, а не изменения целевой функции модели. Иными словами, цель — ускорить и повысить эффективность дообучения при сохранении точности.

Источник: How to Make LLM Training Faster with Unsloth and NVIDIA
Улучшенная производительность обучения
Отчёт о сотрудничестве приводит несколько улучшений производительности, включая:
- На 14,3% быстрее на батч в бенчмарке Qwen3-14B QLoRA SFT благодаря кэшированию метаданных упакованных последовательностей
- Ускорение на 8,4% для моделей 8B, на 6,7% для 14B и на 4,6% для 32B за счёт асинхронного чекпоинтинга градиентов с двойным буфером
- Около 10–15% ускорения для обучения GPT-OSS MoE, с ускорением прямого прохода на 23% и обратного — на 13% на целевом маршруте
Наибольшие приросты производительности от сотрудничества Unsloth и NVIDIA относятся к упакованному текстовому обучению и моделям Mixture-of-Experts. Мы их здесь не используем, поскольку наш конвейер сосредоточен на дообучении Qwen3.5 Vision для OCR.
В этом руководстве мы используем NVIDIA RTX 3090, поэтому конвейер построен вокруг ускорения на GPU NVIDIA и оптимизированного пути дообучения Unsloth. Мы не сравниваем Unsloth с другим тренером, поэтому не стоит воспринимать это руководство как независимое подтверждение заявленных ускорений. Вместо этого мы применяем оптимизированный конвейер Unsloth к реальной задаче «визуализация + язык».
Чекпоинтинг градиентов
Для этого конвейера наиболее актуальная оптимизация — чекпоинтинг градиентов Unsloth. Он помогает снизить потребление памяти во время обучения, избегая необходимости хранить каждую активацию в памяти GPU. Это особенно полезно при дообучении моделей «визуализация + язык», где модель обрабатывает и изображения, и текст.
1. Подготовка Unsloth для более быстрого дообучения
Чтобы пройти это руководство, вам нужен доступ к GPU NVIDIA. Вы можете арендовать его на платформах вроде RunPod, Vast.ai или у любого другого облачного провайдера GPU. Сначала я пробовал RunPod, так как он обычно быстрый и надёжный, но в тот момент вариантов RTX 3090 было мало. Поэтому я использовал машину Vast.ai с RTX 3090 для этого конвейера.
Для сравнения разных платформ ознакомьтесь с нашим руководством по лучшими облачными провайдерам GPU.

Источник: Vast.ai | Console
После запуска инстанса я открыл Jupyter Notebook и создал новый блокнот. На Vast.ai я выбрал доступное ядро основной среды, чтобы установить необходимые пакеты Python в среде блокнота, не затрагивая системные зависимости.
Установка необходимых пакетов
Сначала установите необходимые пакеты для Unsloth, PyTorch, обучения моделей по изображениям, загрузки датасетов и интеграции с 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
Эти пакеты основаны на официальной настройке ноутбука Unsloth и включают основные библиотеки, необходимые для загрузки Qwen3.5 Vision, подготовки пар «изображение-текст» и дообучения модели с Unsloth.
Настройка устройства CUDA
Далее мы настроим устройство CUDA и проверим, что доступен нужный GPU NVIDIA. Поскольку в руководстве используется RTX 3090, код проверяет, включена ли CUDA, подтверждает выбранный GPU, печатает версии CUDA и PyTorch и удостоверяется, что на машине достаточно VRAM для эксперимента.
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.")
В моей конфигурации среда вернула следующую информацию о 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
Это подтверждает, что блокнот запущен на NVIDIA GeForce RTX 3090 с достаточным объёмом VRAM для эксперимента по дообучению.
Определение настроек обучения и подсказок
После проверки GPU определим модель, датасет, параметры обучения, каталоги вывода, размер изображений и подсказки для 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."
Здесь мы используем Qwen3.5 Vision 4B от Unsloth и медицинский датасет для OCR по изображениям. Для этого руководства выбираем 300 примеров и обучаем 30 шагов — этого достаточно, чтобы показать, как модель подстраивается под целевой формат OCR, сохраняя лёгкость запуска.
Фиксированный размер 768×1024 помогает поддерживать единообразие входных изображений во время обучения. Медицинские документы сильно варьируются по разрешению и соотношению сторон, поэтому приведение к фиксированному холсту делает конвейер более стабильным и снижает проблемы с формами при дообучении моделей «визуализация + язык».
2. Загрузка модели
Теперь, когда среда готова, загрузим модель Qwen3.5 Vision 4B с помощью Unsloth’s 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)
После загрузки модель сообщает, что загружена нужная версия, включён 4-битный режим и модель размещена на GPU:
Loaded: unsloth/Qwen3.5-4B
4-bit: True
Model device: cuda:0
Здесь FastVisionModel.from_pretrained() загружает модель «визуализация + язык» и применяет оптимизации Unsloth для более быстрого и экономного по памяти дообучения. Мы также включаем load_in_4bit, что снижает использование VRAM за счёт загрузки модели в 4-битной точности. Это полезно при работе с 24‑гигабайтными GPU, такими как RTX 3090.
Мы также включаем чекпоинтинг градиентов Unsloth с помощью use_gradient_checkpointing="unsloth"
Это помогает снизить потребление памяти во время обучения, что особенно важно для моделей «визуализация + язык», обрабатывающих и изображения, и текст.
3. Добавление адаптеров LoRA
Далее добавим к модели адаптеры LoRA. LoRA позволяет дообучать небольшой набор обучаемых параметров вместо обновления всей модели. Это делает обучение быстрее, экономичнее по памяти и удобнее для запуска на одном 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,
)
В рамках этого руководства адаптеры добавляются и к визуальной, и к языковой частям модели. Это помогает модели научиться читать изображения медицинских документов и выдавать ожидаемый структурированный текст OCR. После этого шага модель готова к обучению на медицинском датасете OCR.
4. Загрузка медицинского датасета OCR
Теперь загрузим медицинский датасет OCR из Hugging Face и подготовим небольшой поднабор для дообучения.
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>",
]
Датасет содержит изображения документов и соответствующий им текст OCR. Поскольку в этом руководстве нам нужны примеры в «медицинском стиле», мы фильтруем датасет простым подходом на основе ключевых слов. Код ищет термины, часто встречающиеся в медицинских документах, такие как doctor, clinic, patient, medication, prescription, diagnosis и слова, связанные с дозировками.
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.")
Это даёт лёгкий способ выбрать примеры, релевантные медицинскому OCR. В этом запуске мы берём 300 таких примеров.
Далее нормализуем каждое изображение до фиксированного холста 768×1024. Изображения медицинских документов могут иметь разные размеры и соотношения сторон, поэтому этот шаг повышает согласованность обучающих данных. Изображение масштабируется с сохранением исходного соотношения сторон, затем помещается на белый фон.
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
Вместо использования datasets.map мы вручную строим простой список Python. Это позволяет избежать возможных зависаний в некоторых облачных средах блокнотов при перезаписи изображений 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"])
После предобработки каждый пример содержит два поля: нормализованное изображение и целевой текст 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>
Также можно предварительно просмотреть один из изменённых по размеру примеров:
dataset[EVAL_INDEX]["image"].resize((384, 512))
Предпросмотр показывает изображение документа медицинского типа с данными клиники, именем врача, информацией о пациенте, препаратами и подписью. Это подтверждает, что датасет подходит для задачи дообучения OCR.

5. Преобразование примеров в формат «визуальный диалог»
Теперь, когда датасет загружен и изображения нормализованы, нам нужно преобразовать каждый пример в формат диалога, ожидаемый Qwen3.5 Vision.
Каждый обучающий пример должен включать три части:
- Системное сообщение, задающее роль модели как движка медицинского OCR
- Сообщение пользователя, содержащее изображение и инструкцию для OCR
- Сообщение ассистента с ожидаемым выводом 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
Эта вспомогательная функция создаёт структуру сообщений и для обучения, и для инференса. Во время обучения мы включаем целевой текст OCR как ответ ассистента. Во время инференса мы даём только изображение и инструкцию и просим модель сгенерировать текст OCR.
Далее преобразуем каждый пример датасета в этот формат диалога:
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]
После преобразования каждый пример содержит список сообщений. В первом примере есть системная подсказка, изображение медицинского документа, инструкция для OCR и ожидаемая структурированная транскрипция OCR. Такой формат позволяет модели учиться сопоставлять изображение и инструкцию с корректным текстовым выводом.
{'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. Оценка базовой модели до дообучения
Перед обучением стоит протестировать базовую модель на одном примере OCR. Это даст нам точку отсчёта для сравнения результатов до и после дообучения.
Сначала определим вспомогательную функцию для применения chat-шаблона модели. Некоторые версии токенизатора поддерживают enable_thinking=False, другие — нет, поэтому функция включает запасной вариант для совместимости.
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,
)
Затем определим функцию генерации. Она строит подсказку для OCR, передаёт токенизатору и изображение, и текстовые инструкции, генерирует вывод модели и декодирует только заново сгенерированные токены.
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]
Теперь переключим модель в режим инференса и сгенерируем текст OCR для первого изображения для оценки:
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)
Вывод базовой модели читаем, но он не полностью соответствует целевой структуре:
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
Это полезная отправная точка. Базовая модель уже умеет считывать большую часть документа, но выдаёт текст в «естественном» стиле OCR, а не в структурированном формате, используемом в датасете. Дообучение должно помочь выровнять ответы под целевой формат и сделать их более последовательными.
7. Обучение модели
Теперь, когда датасет в корректном формате «визуальный диалог», можно обучать модель с помощью SFTTrainer из TRL и vision-коллатора 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()
Сначала переводим модель в режим обучения с FastVisionModel.for_training(model). Затем создаём тренер, используя преобразованный датасет OCR.
Важная часть здесь — UnslothVisionDataCollator. Поскольку это задача «визуализация + язык», тренеру нужно корректно обрабатывать и изображения медицинских документов, и целевой текст OCR. Коллатор подготавливает эти мультимодальные примеры для передачи модели при обучении с учителем.
В этом руководстве мы обучаем 30 шагов с размером батча на устройство 4 и аккумуляцией градиентов 2, что даёт эффективный размер батча 8. Так запуск остаётся лёгким, но уже видно, как модель начинает подстраиваться под структурированный формат OCR.

Во время обучения Unsloth выводит полезную информацию о настройке: количество примеров, шагов и батчей, число обучаемых параметров и возможности экономии памяти. В этом запуске Unsloth сообщает, что для обратного прохода включён двойной буфер, что сокращает время ожидания при чекпоинтинге градиентов.
8. Оценка дообученной модели
После обучения переключим модель обратно в режим инференса и сгенерируем текст OCR для того же изображения, что использовалось до дообучения.
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)
После дообучения вывод модели стал намного ближе к целевой структуре из датасета:
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>
Это показывает, что дообученная модель освоила ожидаемый формат ответа OCR. Базовая модель уже могла извлечь большую часть видимого текста, но дообучение помогло выровнять вывод под структурированный формат, используемый в обучающих данных.
Также можно протестировать модель на другом примере из датасета:
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)
Во втором примере модель соблюдает ожидаемую структуру, но допускает небольшую ошибку OCR, генерируя Amoxicillin вместо 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>
Это полезное напоминание, что модель улучшилась в соблюдении формата, но точность OCR по‑прежнему зависит от качества данных, чёткости изображений, размера обучения и числа шагов дообучения. Для промышленной системы OCR потребуется обучение на большем и более разнообразном датасете и оценка точности на широком наборе примеров.
9. Сохранение дообученного адаптера
По завершении обучения сохраняем адаптер LoRA и токенизатор локально.
model.save_pretrained(ADAPTER_DIR)
tokenizer.save_pretrained(ADAPTER_DIR)
print("Saved adapter to:", ADAPTER_DIR)
Вывод подтверждает, что адаптер сохранён:
Saved adapter to: qwen35-vision-medical-ocr-lora
Сохраняются только веса дообученного адаптера, а не полная копия базовой модели. Позже вы сможете заново загрузить базовую Qwen3.5-4B и применить этот адаптер, чтобы переиспользовать дообученное поведение OCR. Это делает сохранённую модель лёгкой для хранения, обмена и развёртывания.
Итоги
Процесс обучения оказался лёгким и практичным на одном NVIDIA RTX 3090. Хотя дообучение моделей «визуализация + язык» обычно требовательно к памяти, использование VRAM оказалось значительно ниже ожидаемого. Пиковое потребление было около 14 ГБ, а среднее — ближе к 9 ГБ, что впечатляет для дообучения модели Qwen3.5 Vision.
Модель также адаптировалась быстро. Уже после нескольких шагов обучения вывод стал заметно ближе к целевой структуре OCR. Базовая модель умела читать документ, а после дообучения стала стабильнее следовать формату датасета.
Тем не менее опыт настройки был не идеальным. Установка Unsloth потребовала множества проб и ошибок. Настроить его корректно бывает непросто, особенно при работе в разных локальных и виртуальных средах, с разными версиями CUDA и у разных облачных провайдеров GPU.
Иногда проблемы совместимости с CUDA могут «сломать» среду, и отладка таких сбоев занимает больше времени, чем ожидалось. Даже старт с Docker-образа Unsloth на облачной платформе GPU может оказаться трудозатратным, если среда не работает «из коробки».
Ещё один важный урок — шаблон модели имеет значение. Если датасет не преобразован в корректный chat- или «визуальный диалог»-формат, модель может не обучаться должным образом. Для Qwen3.5 Vision крайне важно использовать правильную структуру сообщений «изображение + текст». Без подходящего шаблона обучение может идти, но модель не будет реально адаптироваться к задаче.
В целом Unsloth — сильный вариант для пользователей с ограниченным доступом к GPU, которые хотят эффективно дообучать модели на локальных машинах или арендованных GPU. Он снижает использование памяти, делает более скромное «железо» полезнее и может ускорить эксперименты. Однако для тех, кто регулярно дообучает и обучает модели, сложность настройки может раздражать. Стандартное обучение на Transformers часто стабильнее, проще в установке и легче воспроизводится в разных средах.
Если вас отпугивает именно трение при установке, рекомендуем прочитать наш гид по Unsloth Studio, где показано, как дообучить Qwen3.5-9B без ручной настройки среды — в локальном веб‑интерфейсе Unsloth.