Courses
ความร่วมมือของ Unsloth กับ NVIDIA มุ่งเน้นการทำให้การปรับจูนรวดเร็วยิ่งขึ้นด้วยการลดค่าใช้จ่ายแฝงในการเทรน แทนที่จะพึ่งเฉพาะ GPU ขนาดใหญ่ขึ้นหรือโมเดลเล็กลง การปรับปรุงต่างๆ จะเล็งไปที่คอขวดภายในกระบวนการเทรน เช่น การสร้างเมตาดาต้าซ้ำซ้อน ความล่าช้าในการโหลดแอคติเวชัน และการกระจายโทเคนที่ไม่มีประสิทธิภาพ สำหรับผู้ใช้ นั่นหมายถึงการเทรนที่เร็วขึ้นและเวิร์กโฟลว์การปรับจูนที่ลื่นไหลขึ้นบน GPU ของ NVIDIA ที่รองรับ
ในคู่มือนี้ เราจะเรียนรู้เกี่ยวกับการปรับปรุงประสิทธิภาพใหม่ของ Unsloth เหล่านี้และประยุกต์ใช้เวิร์กโฟลว์การปรับจูนที่Unsloth ปรับแต่งให้เหมาะสมกับงานวิสันน์-ภาษาที่ใช้งานได้จริง เราจะปรับจูนQwen3.5 Vision 4Bสำหรับงาน OCR ทางการแพทย์ โดยให้โมเดลเรียนรู้การดึงข้อความเชิงโครงสร้างจากภาพเอกสารการแพทย์ โดยใช้ชุดย่อยขนาดเล็กของชุดข้อมูล OCR ที่ดูเหมือนเอกสารทางการแพทย์
เราจะใช้:
- Qwen3.5 4B (Vision) เป็นโมเดลฐาน
- QLoRA 4 บิต เพื่อลดการใช้ VRAM
- อะแดปเตอร์ LoRA เพื่อการปรับจูนที่มีประสิทธิภาพ
- Unsloth gradient checkpointing เพื่อประหยัดหน่วยความจำระหว่างการเทรน
- ชุดย่อย 300 ตัวอย่างจากชุดข้อมูล OCR ทางการแพทย์
- การเตรียมภาพให้มีขนาดคงที่เพื่อการเทรนด้านวิสันน์ที่ลื่นไหล
- การประเมินก่อนและหลังเพื่อเปรียบเทียบเอาต์พุตของโมเดลฐานและโมเดลที่ปรับจูนแล้ว
การใช้เวิร์กโฟลว์การปรับจูนที่ปรับให้เหมาะกับ NVIDIA ของ Unsloth
ก่อนเริ่มปรับจูน ควรทำความเข้าใจว่าความร่วมมือของ Unsloth กับ NVIDIAช่วยปรับปรุงอะไรบ้าง และเชื่อมโยงกับคู่มือนี้อย่างไร
Unsloth รายงานว่าความร่วมมือกับ NVIDIA ทำให้การเทรน LLMเร็วขึ้นราว 25% โดยไม่สูญเสียความแม่นยำ เมื่อเทียบกับการเพิ่มความเร็ว 2–5 เท่าของการปรับจูนที่มีอยู่แล้ว ผลลัพธ์นี้มาจากการลดค่าใช้จ่ายแฝงรอบๆ กระบวนการเทรนหลัก มากกว่าการเปลี่ยนวัตถุประสงค์การเรียนรู้ของโมเดล กล่าวคือ เป้าหมายคือทำให้การปรับจูนเร็วและมีประสิทธิภาพยิ่งขึ้น โดยคงความแม่นยำเท่าเดิม

ที่มา: How to Make LLM Training Faster with Unsloth and NVIDIA
ประสิทธิภาพการเทรนที่ดีขึ้น
รายงานความร่วมมือดังกล่าวสรุปการปรับปรุงประสิทธิภาพหลายด้าน ได้แก่:
- เร็วขึ้นต่อแบตช์ 14.3% บน benchmark Qwen3-14B QLoRA SFT ด้วยการแคชเมตาดาต้าสำหรับ packed-sequence
- เพิ่มความเร็ว 8.4% บนโมเดล 8B, 6.7% บน 14B และ 4.6% บน 32B ด้วย double-buffered async gradient checkpointing
- เพิ่มความเร็วราว 10–15% สำหรับการเทรน GPT-OSS MoE โดยเร็วขึ้น 23% ใน forward และ 13% ใน backward บนเส้นทาง routing ที่กำหนดเป้าหมาย
ประโยชน์ด้านประสิทธิภาพที่มากที่สุดบางส่วนจากความร่วมมือระหว่าง Unsloth และ NVIDIA จะใช้ได้ดีกับการเทรนแบบ packed ที่เป็นข้อความล้วนและโมเดล Mixture-of-Experts ในคู่มือนี้เราไม่ได้ใช้แนวทางเหล่านั้น เพราะเวิร์กโฟลว์ของเรามุ่งไปที่การปรับจูน Qwen3.5 Vision สำหรับงาน OCR
ในคู่มือนี้ เราใช้ GPU NVIDIA RTX 3090 ดังนั้นเวิร์กโฟลว์จึงอาศัยการเร่งความเร็วด้วย GPU ของ NVIDIA และเส้นทางการปรับจูนที่ปรับให้เหมาะของ Unsloth เราไม่ได้ทำ benchmark เปรียบเทียบ Unsloth กับตัวเทรนอื่น ดังนั้นคู่มือนี้ไม่ควรถูกมองว่าเป็นหลักฐานอิสระของการเพิ่มความเร็วที่รายงานไว้ แต่เป็นการประยุกต์ใช้เวิร์กโฟลว์การปรับจูนของ Unsloth กับงานวิสันน์-ภาษาจริง
Gradient checkpointing
สำหรับเวิร์กโฟลว์นี้ การปรับให้เหมาะที่เกี่ยวข้องมากที่สุดคือ gradient checkpointing ของ Unsloth ซึ่งช่วยลดการใช้หน่วยความจำระหว่างการเทรนโดยไม่ต้องเก็บแอคติเวชันทุกตัวไว้ในหน่วยความจำของ GPU สิ่งนี้มีประโยชน์อย่างยิ่งกับการปรับจูนวิสันน์-ภาษา ซึ่งโมเดลต้องประมวลผลทั้งอินพุตภาพและเอาต์พุตข้อความ
1. ติดตั้ง Unsloth สำหรับการปรับจูนที่เร็วขึ้น
เพื่อทำตามคู่มือนี้ จำเป็นต้องเข้าถึง GPU ของ NVIDIA สามารถเช่าได้จากแพลตฟอร์มอย่าง RunPod, Vast.ai หรือผู้ให้บริการคลาวด์ GPU รายอื่น เดิมทีฉันลองใช้ RunPod เพราะโดยปกติแล้วรวดเร็วและเสถียร แต่ช่วงนั้นตัวเลือก RTX 3090 มีจำกัด จึงหันไปใช้เครื่อง GPU RTX 3090 บน Vast.aiสำหรับเวิร์กโฟลว์นี้
สำหรับการเปรียบเทียบแพลตฟอร์มต่างๆ โปรดดูคู่มือของเราเกี่ยวกับ ผู้ให้บริการคลาวด์ 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 โดยใช้ FastVisionModel ของ 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)
หลังจากโหลดโมเดลแล้ว เอาต์พุตจะยืนยันว่าโหลดโมเดลที่ถูกต้อง เปิดใช้งานโหมด 4 บิต และวางโมเดลไว้บน GPU แล้ว:
Loaded: unsloth/Qwen3.5-4B
4-bit: True
Model device: cuda:0
ที่นี่ FastVisionModel.from_pretrained() จะโหลดโมเดลวิสันน์-ภาษาและประยุกต์ใช้การปรับให้เหมาะของ Unsloth เพื่อการปรับจูนที่เร็วขึ้นและประหยัดหน่วยความจำ เรายังเปิดใช้ load_in_4bit ซึ่งลดการใช้ VRAM โดยโหลดโมเดลในความละเอียด 4 บิต ซึ่งมีประโยชน์เมื่อทำงานกับ GPU ขนาด 24 GB อย่าง RTX 3090
เรายังเปิดใช้ gradient checkpointing ของ 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 ที่สอดคล้องกัน เนื่องจากเราอยากได้ตัวอย่าง 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 หนึ่งตัว เพื่อให้ได้จุดอ้างอิงสำหรับเปรียบเทียบเอาต์พุตก่อนและหลังการปรับจูน
อันดับแรก เรานิยามฟังก์ชันช่วยเพื่อใช้เทมเพลตแชตของโมเดล เวอร์ชันของโทเคไนเซอร์บางตัวรองรับ 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 โดยใช้คอลเลเตอร์ข้อมูลวิสันน์ของ 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 สเต็ป ด้วย batch size ต่ออุปกรณ์ 4 และสะสมกราเดียนต์ 2 ให้ batch ขนาดมีผลเท่ากับ 8 ซึ่งทำให้การรันเบา แต่ยังแสดงให้เห็นว่าโมเดลเริ่มปรับตัวเข้ากับรูปแบบ OCR เชิงโครงสร้างอย่างไร

ระหว่างการเทรน Unsloth จะแสดงข้อมูลที่มีประโยชน์เกี่ยวกับการตั้งค่า รวมถึงจำนวนตัวอย่าง สเต็ปและแบตช์ จำนวนพารามิเตอร์ที่ฝึกได้ และคุณสมบัติการประหยัดหน่วยความจำ สำหรับการรันนี้ Unsloth รายงานว่าได้เปิดใช้ double buffering สำหรับ backward pass ซึ่งช่วยลดเวลารอระหว่าง gradient checkpointing
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)
สำหรับตัวอย่างที่สองนี้ โมเดลทำตามโครงสร้างที่คาดหวัง แต่เกิดข้อผิดพลาดเล็กน้อยโดยสร้างคำว่า 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 GB ขณะที่ค่าเฉลี่ยอยู่ใกล้ 9 GB ซึ่งน่าประทับใจสำหรับการปรับจูนโมเดล Qwen3.5 Vision
โมเดลยังปรับตัวได้รวดเร็ว หลังเพียงไม่กี่สเต็ป เอาต์พุตก็ใกล้เคียงกับโครงสร้าง OCR เป้าหมายมากขึ้น โมเดลฐานอ่านเอกสารได้อยู่แล้ว แต่หลังการปรับจูน โมเดลทำตามรูปแบบของชุดข้อมูลได้สม่ำเสมอกว่า
อย่างไรก็ดี ประสบการณ์การตั้งค่าไม่ได้ราบรื่นทั้งหมด การติดตั้ง Unsloth ต้องลองผิดลองถูกมากพอสมควร ซึ่งอาจตั้งค่าได้ยาก โดยเฉพาะเมื่อต้องทำงานข้ามสภาพแวดล้อมโลคอลหลายแบบ สภาพแวดล้อมเสมือน เวอร์ชัน CUDA และผู้ให้บริการคลาวด์ GPU ที่ต่างกัน
ในบางกรณี ปัญหาความเข้ากันได้ของ CUDA อาจทำให้สภาพแวดล้อมพัง และการดีบักอาจใช้เวลามากกว่าที่คาด แม้จะเริ่มจากอิมเมจ Docker ของ Unsloth บนแพลตฟอร์มคลาวด์ GPU ก็ยังอาจใช้เวลามาก หากสภาพแวดล้อมไม่ทำงานได้อย่างราบรื่นตั้งแต่แรก
อีกบทเรียนสำคัญคือเทมเพลตของโมเดลมีความสำคัญ หากชุดข้อมูลไม่ได้ถูกแปลงให้อยู่ในรูปแบบแชตหรือบทสนทนาวิสันน์ที่ถูกต้อง โมเดลอาจไม่เรียนรู้อย่างเหมาะสม สำหรับ Qwen3.5 Vision การใช้โครงสร้างข้อความภาพที่ถูกต้องเป็นสิ่งจำเป็น หากไม่มีเทมเพลตที่ถูกต้อง การเทรนอาจรันได้ แต่โมเดลอาจไม่ปรับตัวกับงานจริง
โดยรวมแล้ว Unsloth เป็นตัวเลือกที่แข็งแกร่งสำหรับผู้ใช้ที่เข้าถึง GPU ได้จำกัดและต้องการปรับจูนโมเดลอย่างมีประสิทธิภาพบนเครื่องโลคอลหรือ GPU เช่า มันช่วยลดการใช้หน่วยความจำ ทำให้ฮาร์ดแวร์ขนาดเล็กมีประโยชน์ยิ่งขึ้น และสามารถเร่งการทดลองได้ อย่างไรก็ตาม สำหรับผู้ใช้ที่ต้องปรับจูนและเทรนโมเดลเป็นประจำ ความซับซ้อนในการตั้งค่าอาจน่าหงุดหงิด การเทรนแบบ Transformers มาตรฐานมักเสถียรกว่า ติดตั้งง่ายกว่า และทำซ้ำได้ง่ายข้ามสภาพแวดล้อม
หากความยุ่งยากในการติดตั้งเป็นสิ่งที่ทำให้ถอดใจ ขอแนะนำให้อ่านคู่มือ Unsloth Studio ของเรา ซึ่งจะแสดงวิธีปรับจูน Qwen3.5-9B โดยไม่ต้องตั้งค่าสภาพแวดล้อมด้วยตนเอง ในเว็บ UI โลคอลของ Unsloth