Chuyển đến nội dung chính

Gradient Checkpointing: Tinh chỉnh hiệu quả với Unsloth và NVIDIA

Tìm hiểu cách gradient checkpointing tối ưu cho NVIDIA và QLoRA của Unsloth giúp giảm VRAM khi tinh chỉnh Qwen3.5 4B cho OCR tài liệu y tế.
Đã cập nhật 14 thg 8, 2026  · 12 phút đọc

Khám phá với AI

ChatGPTClaudePerplexity

Hợp tác giữa Unsloth và NVIDIA tập trung vào việc tăng tốc tinh chỉnh bằng cách giảm chi phí ẩn trong quá trình huấn luyện. Thay vì chỉ dựa vào GPU lớn hơn hoặc mô hình nhỏ hơn, các cải tiến nhắm vào các nút thắt cổ chai bên trong quy trình huấn luyện, như việc dựng lại metadata lặp lại, độ trễ tải lại activation và định tuyến token kém hiệu quả. Với người dùng, điều này đồng nghĩa huấn luyện nhanh hơn và quy trình tinh chỉnh mượt mà hơn trên các GPU NVIDIA được hỗ trợ.

Trong hướng dẫn này, chúng ta sẽ tìm hiểu về các cải thiện hiệu năng mới của Unsloth và áp dụng quy trình tinh chỉnh Unsloth được tối ưu vào một tác vụ thị giác–ngôn ngữ thực tế. Chúng ta sẽ tinh chỉnh Qwen3.5 Vision 4B cho OCR y tế, nơi mô hình học cách trích xuất văn bản có cấu trúc từ ảnh tài liệu y tế bằng một tập con nhỏ trông giống dữ liệu OCR y tế.

Chúng ta sẽ dùng:

  • Qwen3.5 4B (Vision) làm mô hình gốc
  • QLoRA 4-bit để giảm sử dụng VRAM
  • Bộ chuyển LoRA để tinh chỉnh hiệu quả
  • Gradient checkpointing của Unsloth để tiết kiệm bộ nhớ khi huấn luyện
  • Tập con 300 mẫu của một bộ dữ liệu OCR y tế
  • Tiền xử lý ảnh kích thước cố định để huấn luyện thị giác mượt mà hơn
  • Đánh giá trước–sau để so sánh đầu ra mô hình gốc và mô hình đã tinh chỉnh

Sử dụng quy trình tinh chỉnh tối ưu cho NVIDIA của Unsloth

Trước khi bắt đầu tinh chỉnh, sẽ hữu ích nếu hiểu hợp tác NVIDIA của Unsloth cải thiện điều gì và nó liên quan thế nào đến hướng dẫn này.

Unsloth cho biết hợp tác với NVIDIA giúp huấn luyện LLM nhanh hơn khoảng 25%, không giảm độ chính xác, bên cạnh các tăng tốc 2–5 lần trong tinh chỉnh đã có. Các mức tăng này đến từ việc giảm chi phí ẩn xung quanh quy trình huấn luyện chính thay vì thay đổi mục tiêu học của mô hình. Nói cách khác, mục tiêu là làm cho tinh chỉnh nhanh hơn và hiệu quả hơn trong khi vẫn giữ nguyên độ chính xác.

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

Nguồn: How to Make LLM Training Faster with Unsloth and NVIDIA 

Hiệu năng huấn luyện cải thiện

Bản hợp tác báo cáo một số cải thiện hiệu năng, bao gồm:

  • Nhanh hơn 14,3% mỗi batch trên benchmark Qwen3-14B QLoRA SFT nhờ bộ nhớ đệm metadata packed-sequence
  • Tăng tốc 8,4% trên mô hình 8B, 6,7% trên mô hình 14B và 4,6% trên mô hình 32B nhờ gradient checkpointing không đồng bộ double-buffered
  • Tăng tốc khoảng 10–15% cho huấn luyện GPT-OSS MoE, với 23% nhanh hơn ở forward và 13% nhanh hơn ở backward trong đường định tuyến mục tiêu

Một số mức tăng hiệu năng lớn nhất từ hợp tác Unsloth và NVIDIA áp dụng cho huấn luyện văn bản thuần dạng packed và các mô hình Mixture-of-Experts. Chúng ta không dùng các phần đó trong hướng dẫn này vì quy trình của chúng ta tập trung vào tinh chỉnh OCR cho Qwen3.5 Vision. 

Trong hướng dẫn này, chúng ta dùng GPU NVIDIA RTX 3090, nên quy trình xoay quanh tăng tốc GPU NVIDIA và đường tinh chỉnh tối ưu của Unsloth. Chúng ta không benchmark Unsloth với trình huấn luyện khác, vì vậy hướng dẫn này không nên được xem là bằng chứng độc lập cho các mức tăng tốc đã báo cáo. Thay vào đó, chúng ta áp dụng quy trình tinh chỉnh tối ưu của Unsloth cho một tác vụ thị giác–ngôn ngữ thực tế.

Gradient checkpointing

Với quy trình này, tối ưu hóa liên quan nhất là gradient checkpointing của Unsloth. Nó giúp giảm sử dụng bộ nhớ trong quá trình huấn luyện bằng cách tránh phải lưu mọi activation trong bộ nhớ GPU. Điều này đặc biệt hữu ích cho tinh chỉnh thị giác–ngôn ngữ, nơi mô hình phải xử lý cả đầu vào ảnh và đầu ra văn bản.

1. Thiết lập Unsloth cho tinh chỉnh nhanh hơn

Để chạy hướng dẫn này, bạn cần có GPU NVIDIA. Bạn có thể thuê từ các nền tảng như RunPod, Vast.ai, hoặc bất kỳ nhà cung cấp GPU đám mây nào khác. Ban đầu tôi thử dùng RunPod vì thường nhanh và ổn định, nhưng lúc đó lựa chọn RTX 3090 hạn chế. Vì vậy tôi đã dùng một máy GPU Vast.ai RTX 3090 cho quy trình này. 

Để so sánh các nền tảng khác nhau, hãy xem hướng dẫn về các nhà cung cấp GPU đám mây tốt nhất.

Vast.ai RTX 3090 instance

Nguồn:  Vast.ai | Console 

Sau khi khởi chạy instance, tôi mở Jupyter Notebook và tạo một notebook mới. Trên Vast.ai, tôi chọn kernel môi trường chính sẵn có để cài các gói Python cần thiết ngay trong môi trường notebook mà không ảnh hưởng đến phụ thuộc ở cấp hệ thống. 

Cài đặt các gói cần thiết

Đầu tiên, cài đặt các gói cần thiết cho Unsloth, PyTorch, huấn luyện mô hình thị giác, tải bộ dữ liệu và tích hợp 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

Các gói này dựa trên thiết lập notebook chính thức của Unsloth và bao gồm các thư viện chính để tải Qwen3.5 Vision, chuẩn bị dữ liệu ảnh–văn bản, và tinh chỉnh mô hình với Unsloth. 

Cấu hình thiết bị CUDA

Tiếp theo, chúng ta cấu hình thiết bị CUDA và xác minh GPU NVIDIA phù hợp đang sẵn sàng. Vì hướng dẫn này dùng RTX 3090, đoạn mã sẽ kiểm tra CUDA có bật không, xác nhận GPU đã chọn, in phiên bản CUDA và PyTorch, và kiểm tra máy có đủ VRAM cho thí nghiệm này hay không. 

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

Trong thiết lập của tôi, môi trường trả về cấu hình GPU như sau:

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

Điều này xác nhận notebook đang chạy trên NVIDIA GeForce RTX 3090 với đủ VRAM cho thí nghiệm tinh chỉnh.

Định nghĩa thiết lập huấn luyện và lời nhắc

Sau khi xác minh GPU, chúng ta định nghĩa mô hình, bộ dữ liệu, thiết lập huấn luyện, thư mục đầu ra, kích thước ảnh và các lời nhắc 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."

Ở đây, chúng ta dùng Qwen3.5 Vision 4B từ Unsloth và bộ dữ liệu thị giác OCR y tế. Trong hướng dẫn này, chúng ta chọn 300 mẫu và huấn luyện 30 bước, giúp lượt chạy gọn nhẹ nhưng vẫn thể hiện cách mô hình thích nghi với định dạng OCR mục tiêu.

Kích thước ảnh cố định 768×1024 giúp giữ đầu vào ảnh nhất quán trong quá trình huấn luyện. Tài liệu y tế có thể khác nhau nhiều về độ phân giải và tỉ lệ khung hình, nên việc chỉnh vào một khung cố định giúp quy trình mượt hơn và giảm vấn đề liên quan đến kích thước trong tinh chỉnh thị giác–ngôn ngữ.

2. Tải mô hình

Khi môi trường đã sẵn sàng, chúng ta có thể tải mô hình Qwen3.5 Vision 4B bằng FastVisionModel của 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)

Sau khi tải mô hình, đầu ra xác nhận đúng mô hình đã được tải, chế độ 4-bit được bật và mô hình đã nằm trên GPU: 

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

Tại đây, FastVisionModel.from_pretrained() tải mô hình thị giác–ngôn ngữ và áp dụng các tối ưu của Unsloth để tinh chỉnh nhanh hơn và tiết kiệm bộ nhớ hơn. Chúng ta cũng bật load_in_4bit, giúp giảm sử dụng VRAM bằng cách tải mô hình ở độ chính xác 4-bit. Điều này hữu ích khi làm việc với GPU 24 GB như RTX 3090.

Chúng ta cũng bật gradient checkpointing của Unsloth với use_gradient_checkpointing="unsloth"

Điều này giúp giảm sử dụng bộ nhớ khi huấn luyện, đặc biệt quan trọng với mô hình thị giác–ngôn ngữ vì chúng xử lý cả đầu vào ảnh và văn bản. 

3. Thêm bộ chuyển LoRA

Tiếp theo, chúng ta thêm bộ chuyển LoRA vào mô hình. LoRA cho phép chúng ta tinh chỉnh một tập tham số huấn luyện nhỏ hơn thay vì cập nhật toàn bộ mô hình. Điều này giúp huấn luyện nhanh hơn, tiết kiệm bộ nhớ và dễ chạy trên một 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,
)

Trong hướng dẫn này, adapter được thêm cho cả phần thị giác và ngôn ngữ của mô hình. Điều này giúp mô hình học cách đọc ảnh tài liệu y tế và tạo ra văn bản OCR có cấu trúc như mong đợi. Sau bước này, mô hình đã sẵn sàng để huấn luyện trên bộ dữ liệu OCR y tế. 

4. Tải bộ dữ liệu OCR y tế

Giờ chúng ta tải bộ dữ liệu OCR y tế từ Hugging Face và chuẩn bị một tập con nhỏ để tinh chỉnh.

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

Bộ dữ liệu chứa ảnh tài liệu và văn bản OCR tương ứng. Vì chúng ta chỉ muốn các ví dụ OCR phong cách y tế cho hướng dẫn này, ta lọc bộ dữ liệu bằng một cách tiếp cận đơn giản dựa trên từ khóa. Mã sẽ tìm các thuật ngữ thường thấy trong tài liệu y tế như bác sĩ, phòng khám, bệnh nhân, thuốc, đơn thuốc, chẩn đoán và các từ liên quan đến liều lượng. 

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

Điều này cho chúng ta một cách nhẹ nhàng để chọn các ví dụ có vẻ phù hợp với tác vụ OCR y tế. Trong lượt chạy này, chúng ta chọn 300 mẫu trông giống y tế.

Tiếp theo, chúng ta chuẩn hóa mỗi ảnh vào một khung 768×1024 cố định. Ảnh tài liệu y tế có thể có kích thước và tỉ lệ khác nhau, nên bước này giúp dữ liệu huấn luyện nhất quán hơn. Ảnh được đổi kích thước giữ nguyên tỉ lệ gốc, rồi đặt lên nền trắng.

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

Thay vì dùng datasets.map, chúng ta tự xây dựng một danh sách Python đơn giản. Điều này tránh các vấn đề treo có thể xảy ra ở một số môi trường notebook đám mây khi ghi lại ảnh 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"])

Sau khi tiền xử lý, mỗi ví dụ chứa hai trường: ảnh đã chuẩn hóa và văn bản OCR mục tiêu. 

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>

Chúng ta cũng có thể xem trước một ví dụ đã đổi kích thước:

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

Bản xem trước cho thấy ảnh tài liệu phong cách y tế với thông tin phòng khám, tên bác sĩ, thông tin bệnh nhân, thuốc và chữ ký. Điều này xác nhận bộ dữ liệu phù hợp cho tác vụ tinh chỉnh OCR. 

Doctors prescription.

5. Chuyển mẫu sang hội thoại thị giác

Khi bộ dữ liệu đã được tải và ảnh đã chuẩn hóa, chúng ta cần chuyển mỗi ví dụ thành định dạng hội thoại mà Qwen3.5 Vision mong đợi.

Mỗi mẫu huấn luyện nên gồm ba phần:

  • Một thông điệp hệ thống xác định vai trò của mô hình là công cụ chuyển âm OCR y tế
  • Một thông điệp người dùng chứa ảnh và hướng dẫn OCR
  • Một thông điệp trợ lý chứa đầu ra OCR mong đợi
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

Hàm trợ giúp trên tạo cấu trúc thông điệp cho cả huấn luyện và suy luận. Trong huấn luyện, chúng ta bao gồm văn bản OCR mục tiêu như phản hồi của trợ lý. Trong suy luận, chúng ta chỉ cung cấp ảnh và hướng dẫn, sau đó yêu cầu mô hình tạo văn bản OCR.

Tiếp theo, chúng ta chuyển đổi mọi mẫu trong bộ dữ liệu sang định dạng hội thoại này:

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]

Sau khi chuyển đổi, mỗi mẫu chứa một danh sách thông điệp. Ví dụ đầu tiên bao gồm lời nhắc hệ thống, ảnh tài liệu y tế, hướng dẫn OCR và bản chuyển âm OCR có cấu trúc mong đợi. Định dạng này cho phép mô hình học cách ánh xạ một ảnh và một hướng dẫn tới đầu ra văn bản đúng. 

{'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. Đánh giá mô hình gốc trước khi tinh chỉnh

Trước khi huấn luyện, chúng ta nên thử mô hình gốc trên một ví dụ OCR. Điều này cho chúng ta mốc tham chiếu để so sánh đầu ra trước và sau tinh chỉnh.

Trước hết, chúng ta định nghĩa một hàm trợ giúp để áp dụng chat template của mô hình. Một số phiên bản tokenizer hỗ trợ enable_thinking=False, trong khi số khác thì không, vì vậy hàm có phương án dự phòng để giữ tương thích.

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

Tiếp theo, chúng ta định nghĩa hàm sinh. Hàm này dựng lời nhắc OCR, truyền cả ảnh và hướng dẫn văn bản cho tokenizer, sinh đầu ra của mô hình và chỉ giải mã phần token mới sinh. 

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]

Giờ chúng ta chuyển mô hình sang chế độ suy luận và tạo văn bản OCR cho ảnh đánh giá đầu tiên:

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)

Đầu ra mô hình gốc có thể đọc được, nhưng không theo đúng cấu trúc mục tiêu:

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

Đây là điểm khởi đầu hữu ích. Mô hình gốc đã có thể đọc phần lớn tài liệu, nhưng xuất văn bản theo phong cách OCR tự nhiên thay vì định dạng có cấu trúc của bộ dữ liệu. Tinh chỉnh sẽ giúp căn chỉnh mô hình với định dạng mục tiêu và khiến phản hồi nhất quán hơn.

7. Huấn luyện mô hình

Khi bộ dữ liệu đã ở đúng định dạng hội thoại thị giác, chúng ta có thể huấn luyện mô hình bằng SFTTrainer của TRL cùng bộ gom dữ liệu thị giác của 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()

Đầu tiên, chúng ta chuyển mô hình sang chế độ huấn luyện với FastVisionModel.for_training(model). Sau đó, tạo trainer dùng bộ dữ liệu OCR đã chuyển đổi.

Điểm quan trọng ở đây là UnslothVisionDataCollator. Vì đây là tác vụ thị giác–ngôn ngữ, trainer cần xử lý đúng cả ảnh tài liệu y tế và văn bản OCR mục tiêu. Collator chuẩn bị các ví dụ đa phương thức này để truyền cho mô hình trong tinh chỉnh có giám sát.

Trong hướng dẫn này, chúng ta huấn luyện 30 bước với batch size mỗi thiết bị là 4 và tích lũy gradient là 2, cho batch hiệu dụng là 8. Điều này giữ lượt chạy gọn nhẹ nhưng vẫn cho thấy mô hình bắt đầu thích nghi với định dạng OCR có cấu trúc.

Fine-tuning the Qwen 3.5 4b Vision language model

Trong quá trình huấn luyện, Unsloth in ra thông tin hữu ích về thiết lập, gồm số lượng ví dụ, bước và batch, số tham số có thể huấn luyện và các tính năng tiết kiệm bộ nhớ. Trong lượt chạy này, Unsloth báo cáo đã bật double buffering cho backward pass, giúp giảm thời gian chờ trong gradient checkpointing.

8. Đánh giá mô hình đã tinh chỉnh

Sau huấn luyện, chúng ta chuyển mô hình lại sang chế độ suy luận và tạo văn bản OCR cho cùng ảnh đánh giá đã dùng trước khi tinh chỉnh.

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)

Sau tinh chỉnh, đầu ra của mô hình gần với cấu trúc mục tiêu của bộ dữ liệu hơn nhiều:

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>

Điều này cho thấy mô hình đã tinh chỉnh học được định dạng phản hồi OCR mong đợi. Mô hình gốc đã có thể trích xuất phần lớn văn bản nhìn thấy, nhưng tinh chỉnh giúp căn chỉnh đầu ra với định dạng có cấu trúc dùng trong dữ liệu huấn luyện.

Chúng ta cũng có thể thử mô hình trên một ví dụ khác từ bộ dữ liệu:

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)

Với ví dụ thứ hai này, mô hình tuân theo cấu trúc mong đợi, nhưng mắc lỗi OCR nhỏ khi tạo Amoxicillin thay vì 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>

Đây là lời nhắc hữu ích rằng mô hình đang cải thiện về sự phù hợp định dạng, nhưng độ chính xác OCR vẫn phụ thuộc vào chất lượng dữ liệu, độ rõ ảnh, kích thước huấn luyện và số bước tinh chỉnh. Với hệ thống OCR sản xuất, bạn sẽ huấn luyện trên bộ dữ liệu lớn hơn, đa dạng hơn và đánh giá độ chính xác trên nhiều ví dụ.

9. Lưu adapter đã tinh chỉnh

Khi huấn luyện hoàn tất, chúng ta lưu adapter LoRA và tokenizer vào máy cục bộ.

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

print("Saved adapter to:", ADAPTER_DIR)

Đầu ra xác nhận adapter đã được lưu:

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

Điều này chỉ lưu trọng số adapter đã tinh chỉnh, không phải bản sao đầy đủ của mô hình gốc. Sau này, bạn có thể tải lại mô hình Qwen3.5-4B gốc và áp dụng adapter này để tái sử dụng hành vi OCR đã tinh chỉnh. Cách này giúp mô hình đã lưu gọn nhẹ, dễ lưu trữ, chia sẻ hoặc triển khai.

Kết luận

Quy trình huấn luyện gọn nhẹ và thực tế trên một NVIDIA RTX 3090. Dù tinh chỉnh thị giác–ngôn ngữ thường tốn bộ nhớ, lượt chạy này dùng ít VRAM hơn nhiều so với dự kiến. Mức VRAM tối đa khoảng 14 GB, trong khi trung bình gần 9 GB, điều này ấn tượng đối với việc tinh chỉnh một mô hình Qwen3.5 Vision.

Mô hình cũng thích nghi nhanh. Chỉ sau vài bước huấn luyện, đầu ra đã gần hơn nhiều với cấu trúc OCR mục tiêu. Mô hình gốc có thể đọc tài liệu, nhưng sau tinh chỉnh, nó theo định dạng của bộ dữ liệu nhất quán hơn. 

Tuy vậy, trải nghiệm thiết lập không hoàn hảo. Cài đặt Unsloth cần rất nhiều thử sai. Có thể khó cấu hình đúng, đặc biệt khi làm việc qua các môi trường cục bộ khác nhau, môi trường ảo, phiên bản CUDA và các nhà cung cấp GPU đám mây. 

Trong một số trường hợp, vấn đề tương thích CUDA có thể phá vỡ môi trường, và gỡ lỗi các vấn đề đó có thể tốn nhiều thời gian hơn dự kiến. Ngay cả khi bắt đầu với image Docker của Unsloth trên nền tảng GPU đám mây cũng có thể tốn thời gian nếu môi trường không hoạt động trơn tru ngay.

Một bài học quan trọng khác là template của mô hình rất quan trọng. Nếu bộ dữ liệu không được chuyển sang đúng định dạng chat hoặc hội thoại thị giác, mô hình có thể sẽ không học đúng cách. Với Qwen3.5 Vision, dùng cấu trúc thông điệp ảnh–văn bản đúng là điều cốt yếu. Nếu không có template phù hợp, huấn luyện có thể chạy, nhưng mô hình có thể không thực sự thích nghi với tác vụ.

Nhìn chung, Unsloth là lựa chọn mạnh mẽ cho người dùng có hạn chế về GPU nhưng muốn tinh chỉnh mô hình hiệu quả trên máy cục bộ hoặc GPU thuê. Nó giảm sử dụng bộ nhớ, khiến phần cứng nhỏ hữu ích hơn và có thể tăng tốc thử nghiệm. Tuy nhiên, với người dùng thường xuyên tinh chỉnh và huấn luyện mô hình, độ phức tạp thiết lập có thể gây khó chịu. Huấn luyện dựa trên Transformers tiêu chuẩn thường ổn định hơn, dễ cài đặt và đơn giản để tái lập trong nhiều môi trường.

Nếu phần gây nản là khâu cài đặt, tôi khuyến nghị đọc hướng dẫn về Unsloth Studio, nơi cho thấy cách tinh chỉnh Qwen3.5-9B mà không cần tự thiết lập môi trường thủ công trong giao diện web cục bộ của Unsloth.


Abid Ali Awan's photo
Author
Abid Ali Awan
LinkedIn
Twitter

Là một nhà khoa học dữ liệu được chứng nhận, tôi đam mê tận dụng công nghệ tiên tiến để tạo ra các ứng dụng học máy đổi mới. Với nền tảng vững chắc về nhận dạng giọng nói, phân tích và báo cáo dữ liệu, MLOps, AI hội thoại và NLP, tôi đã rèn giũa kỹ năng phát triển các hệ thống thông minh có thể tạo ra tác động thực sự. Bên cạnh chuyên môn kỹ thuật, tôi cũng là một người truyền đạt tốt, có khả năng chắt lọc các khái niệm phức tạp thành ngôn ngữ rõ ràng, súc tích. Nhờ đó, tôi trở thành một blogger được nhiều người quan tâm trong lĩnh vực khoa học dữ liệu, chia sẻ góc nhìn và kinh nghiệm với cộng đồng các chuyên gia dữ liệu ngày càng lớn. Hiện tại, tôi tập trung vào sáng tạo và biên tập nội dung, làm việc với các mô hình ngôn ngữ lớn để phát triển nội dung mạnh mẽ và hấp dẫn, giúp doanh nghiệp và cá nhân tận dụng tối đa dữ liệu của mình.

Chủ đề

Các khóa học AI hàng đầu

Courses

Nhập môn Deep Learning với PyTorch

4 giờ
88.5K
Học cách xây dựng mạng nơ-ron đầu tiên của bạn, điều chỉnh các tham số siêu (hyperparameters) và giải quyết các vấn đề phân loại và hồi quy trong PyTorch.
Xem chi tiếtRight Arrow
Bắt Đầu Khóa Học
Xem thêmRight Arrow
Có liên quan

blogs

Claude Opus 4.6: Tính năng, điểm chuẩn, các bài kiểm tra thực hành và hơn thế nữa

Mô hình mới nhất của Anthropic dẫn đầu bảng xếp hạng về mã hóa theo hướng tác nhân và suy luận phức tạp. Thêm nữa, nó có cửa sổ ngữ cảnh 1M.
Matt Crabtree's photo

Matt Crabtree

10 phút

Xem ThêmXem Thêm