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

Fine-tune Qwen3‑VL 8B: Hướng dẫn từng bước

Fine-tune Qwen3‑VL‑8B trên sơ đồ mạch điện tử bằng LoRA. Tìm hiểu toàn bộ quy trình từ chuẩn bị dữ liệu đến xuất bản mô hình lên Hugging Face Hub.
· 10 phút đọc

Khám phá cùng AI

Qwen3‑VL‑8B là mô hình ngôn ngữ‑thị giác mạnh mẽ và được dùng rộng rãi nhất trong dòng Qwen, được thiết kế để hiểu hợp nhất văn bản, hình ảnh và video.

Mô hình mang lại những cải tiến lớn về sinh văn bản, lập luận thị giác, nhận thức không gian, xử lý ngữ cảnh dài và tương tác agent, giúp phù hợp cho cả nghiên cứu và triển khai thực tế trên môi trường biên lẫn đám mây.

Trong hướng dẫn này, chúng ta sẽ fine‑tune Qwen3‑VL‑8B‑Instruct trên các sơ đồ mạch điện tử. Bằng cách huấn luyện mô hình diễn giải ký hiệu, kết nối và quan hệ không gian trên sơ đồ, ta cho phép mô hình hiểu chính xác thiết kế mạch và xác định những linh kiện điện tử cần bổ sung trong mạch thực tế.

Bạn sẽ học cách:

  1. Tải tập dữ liệu từ Hugging Face và làm sạch.
  2. Tạo mẫu chat đa phương thức cho huấn luyện.
  3. Thiết lập pipeline ngôn ngữ‑thị giác Qwen3‑VL.
  4. Cấu hình LoRA để huấn luyện tiết kiệm bộ nhớ.
  5. Fine-tune mô hình và lưu checkpoint.
  6. Xuất bản adapter đã huấn luyện lên Hugging Face.
  7. So sánh kết quả trước và sau fine-tuning.

Nếu bạn còn mới với Hugging Face, lộ trình kỹ năng Hugging Face Fundamentals là dành cho bạn!

1. Thiết lập môi trường để fine-tune Qwen3‑VL‑8B

Khi làm việc với mô hình ngôn ngữ–thị giác, bộ nhớ GPU trở thành ràng buộc quan trọng. Ảnh độ phân giải cao và bộ mã hóa đa phương thức có thể nhanh chóng ngốn VRAM, vì vậy rất khuyến nghị dùng GPU có bộ nhớ dồi dào.

Trong hướng dẫn này, chúng ta sẽ khởi chạy một RunPod A100 (80GB) với image PyTorch mới nhất. Thiết lập này cung cấp đủ khoảng trống VRAM cho huấn luyện và tránh các nút thắt bộ nhớ không cần thiết trong quá trình fine-tuning.

Cuộn xuống phần Pod Template. Tại đây, chọn image PyTorch mới nhất. Đây cũng là nơi bạn có thể điều chỉnh cài đặt lưu trữ và thêm biến môi trường.

Chỉnh sửa PyTorch RunPod

Nhấn Edit, sau đó thực hiện các thay đổi sau:

  • Kích thước đĩa container: đặt 40 GB
  • Kích thước đĩa volume: đặt 40 GB
  • Biến môi trường: Thêm HF_TOKEN và đặt giá trị là Hugging Face access token của bạn (tạo từ Hugging Face Settings)

Cấu hình kích thước đĩa và biến môi trường của RunPod

Hoàn tất, lưu template và triển khai pod.

Tóm tắt Pod

Khi pod đã chạy:

  1. Mở JupyterLab.
  2. Tạo notebook Python mới.
  3. Cài đặt các phụ thuộc cần thiết.

Để cài đặt phụ thuộc, chạy cell mã sau. Trong Jupyter, dấu chấm than ở đầu cho notebook biết thực thi dòng đó như lệnh shell thay vì mã Python.

!pip -q install -U accelerate datasets pillow sentencepiece safetensors peft
!pip install --quiet "transformers==5.0.0rc1"
!pip install --quiet --no-deps trl
!pip install --no-cache-dir flash-attn --no-build-isolation

Tiếp theo, đặt seed ngẫu nhiên cố định để tái lập và bật tối ưu hóa hiệu năng dành riêng cho A100.

import torch
from transformers import set_seed

set_seed(42)

# A100: TF32 gives speedups without changing your bf16 training setup
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True

print("CUDA:", torch.cuda.is_available(), torch.cuda.get_device_name(0) if torch.cuda.is_available() else None)
print("bf16 supported:", torch.cuda.is_available() and torch.cuda.is_bf16_supported())
CUDA: True NVIDIA A100 80GB PCIe
bf16 supported: True

2. Tải bộ dữ liệu Open Schematics từ Hugging Face

Giờ chúng ta sẽ tải bộ dữ liệu Open Schematics từ Hugging Face Hub. Bộ dữ liệu này chứa hình ảnh sơ đồ mạch điện tử cùng siêu dữ liệu phong phú mô tả từng mạch, rất phù hợp cho huấn luyện ngôn ngữ–thị giác.

import torch
from datasets import load_dataset

DATASET_ID = "bshada/open-schematics"

ds_all = load_dataset(DATASET_ID, split="train")
print(ds_all)
Dataset({
    features: ['schematic', 'image', 'components_used', 'json', 'yaml', 'name', 'description', 'type'],
    num_rows: 84470
})

Bộ dữ liệu gồm hơn 84 nghìn mẫu, mỗi mẫu ghép một ảnh sơ đồ với thông tin có cấu trúc như danh sách linh kiện và định dạng máy đọc được (JSON và YAML).

3. Khám phá cấu trúc bộ dữ liệu

Hãy kiểm tra một mẫu đơn lẻ để hiểu rõ hơn cấu trúc dữ liệu.

# quick peek
ex = ds_all[0]
print("\nSample keys:", ex.keys())
print("name:", ex.get("name"))
print("type:", ex.get("type"))
print("components_used:", (ex.get("components_used") or [])[:10])
print("has schematic:", bool(ex.get("schematic")))
print("has json/yaml:", bool(ex.get("json")), bool(ex.get("yaml")))
print("image:", ex.get("image"))
Sample keys: dict_keys(['schematic', 'image', 'components_used', 'json', 'yaml', 'name', 'description', 'type'])
name: TiebeDeclercq/Uart-programmer
type: .kicad_sch
components_used: ['Conn_01x01_Pin', 'Conn_01x06_Pin', 'USB_A', 'Conn_02x05_Odd_Even', 'Conn_01x06_MountingPin', 'C', 'Fuse_Small', 'LED', 'R', 'CH340C']
has schematic: True
has json/yaml: True True
image: <PIL.PngImagePlugin.PngImageFile image mode=RGBA size=1123x794 at 0x7FBC6FD060F0>

Điều này xác nhận mỗi mẫu bao gồm ảnh sơ đồ độ phân giải cao, danh sách linh kiện và biểu diễn có cấu trúc của mạch.

Giờ ta có thể hiển thị ảnh sơ đồ trực tiếp trong notebook Jupyter.

ex.get("image")

Sơ đồ mạch điện tử

Cuối cùng, chúng ta xem danh sách đầy đủ các linh kiện dùng trong sơ đồ này.

print(ex.get("components_used"))
['Conn_01x01_Pin', 'Conn_01x06_Pin', 'USB_A', 'Conn_02x05_Odd_Even', 'Conn_01x06_MountingPin', 'C', 'Fuse_Small', 'LED', 'R', 'CH340C', 'Jumper_3_Open', 'MountingHole', 'AMS1117-3.3', 'MMBT3904', '1N5819HW-7-F', 'LESD5D5.0CT1G', '+3.3V', '+5V', 'GND', 'VCC']

Danh sách linh kiện này cung cấp sự đối chiếu rõ ràng giữa ảnh sơ đồ và các phần tử điện tử có mặt trong mạch.

4. Làm sạch và lọc bộ dữ liệu

Trước khi huấn luyện, chúng ta làm sạch và lọc bộ dữ liệu để đảm bảo mỗi mẫu chứa tối thiểu thông tin cần thiết cho học ngôn ngữ–thị giác. Cụ thể, tập trung giữ lại các ví dụ có chú thích linh kiện hợp lệ và ảnh sơ đồ tương ứng.

Đầu tiên, ta xem có bao nhiêu mẫu thiếu, rỗng hoặc không hợp lệ ở trường components_used.

need_cols = [c for c in ["components_used", "schematic", "name", "type"] if c in ds_all.column_names]
ds_small = ds_all.select_columns(need_cols)

missing_key = none_components = empty_components = missing_any = 0
has_schematic_but_missing = 0

for ex in ds_small:
    if "components_used" not in ex:
        missing_key += 1
        missing_any += 1
        if ex.get("schematic"):
            has_schematic_but_missing += 1
        continue

    cu = ex["components_used"]
    bad = (cu is None) or (isinstance(cu, list) and len(cu) == 0)

    if cu is None:
        none_components += 1
    elif isinstance(cu, list) and len(cu) == 0:
        empty_components += 1

    if bad:
        missing_any += 1
        if ex.get("schematic"):
            has_schematic_but_missing += 1

print("\n=== Missing components report ===")
print("Total:", len(ds_all))
print("Missing key:", missing_key)
print("None:", none_components)
print("Empty list:", empty_components)
print("Missing (any):", missing_any)
if "schematic" in need_cols:
    print("Has schematic but missing components:", has_schematic_but_missing)
=== Missing components report ===
Total: 84470
Missing key: 0
None: 47558
Empty list: 8
Missing (any): 47566
Has schematic but missing components: 47566

Bản tóm tắt cho thấy phần lớn mẫu có dữ liệu sơ đồ nhưng thiếu chú thích linh kiện dùng được.

Để tránh dùng bộ nhớ không cần thiết khi lọc, ta tắt giải mã ảnh. Điều này đảm bảo Hugging Face không tải ảnh vào bộ nhớ khi áp dụng bộ lọc.

from datasets.features import Image as HFImage 
ds_all = ds_all.cast_column("image", HFImage(decode=False))

Sau đó, định nghĩa bộ lọc chỉ giữ các mẫu có danh sách linh kiện không rỗng và tham chiếu ảnh hợp lệ.

def keep_components_and_image(components_used, image):
    # keep only rows with components
    if not (isinstance(components_used, list) and len(components_used) > 0):
        return False

    # image must exist
    if image is None:
        return False

    # when decode=False, image is dict-like: {"path": ...} or {"bytes": ...}
    if isinstance(image, dict):
        return bool(image.get("path")) or bool(image.get("bytes"))

    return True

Áp dụng bộ lọc này giúp giảm đáng kể bộ dữ liệu về các mẫu chất lượng cao, có thể sử dụng đầy đủ.

ds_clean = ds_all.filter(
    keep_components_and_image,
    input_columns=["components_used", "image"],
)

print("Original:", len(ds_all))
print("Clean:", len(ds_clean))
print("Dropped:", len(ds_all) - len(ds_clean))
Original: 84470
Clean: 33275
Dropped: 51195

Sau khi lọc, còn hơn 33 nghìn mẫu sạch chứa cả ảnh sơ đồ hợp lệ và chú thích linh kiện rõ ràng. Bộ dữ liệu đã làm sạch này là nền tảng tin cậy cho các bước tiền xử lý và huấn luyện mô hình tiếp theo.

5. Tải mô hình ngôn ngữ‑thị giác Qwen3‑VL‑8B

Giờ ta tải mô hình Qwen3‑VL‑8B‑Instruct cùng processor tương ứng. Đây là mô hình ngôn ngữ–thị giác quy mô lớn có khả năng suy luận chung giữa ảnh và văn bản, rất phù hợp cho nhiệm vụ hiểu sơ đồ.

Mô hình được tải ở độ chính xác bfloat16 để giảm dùng bộ nhớ nhưng vẫn ổn định số học. Ta cũng bật Flash Attention 2 để attention nhanh hơn và tiết kiệm bộ nhớ trên GPU A100. Tuỳ chọn device_map="auto" tự động đặt các lớp mô hình lên GPU sẵn có.

from transformers import Qwen3VLForConditionalGeneration, AutoProcessor

MODEL_ID = "Qwen/Qwen3-VL-8B-Instruct"

model = Qwen3VLForConditionalGeneration.from_pretrained(
    MODEL_ID,
    dtype=torch.bfloat16,
    device_map="auto",
    attn_implementation="flash_attention_2",
)
processor = AutoProcessor.from_pretrained(MODEL_ID)

6. Thiết kế prompt cho phân tích mạch

Bước này định nghĩa các tiện ích gọn nhẹ để chuẩn bị prompt, mục tiêu và ảnh cho huấn luyện ngôn ngữ–thị giác. Một pipeline duy nhất được dùng cho nhiều tác vụ bằng cách chuyển đổi biến TASK (components, YAML, JSON hoặc tái tạo schematic).

Áp dụng các giới hạn an toàn cơ bản để kiểm soát độ dài mục tiêu và kích thước ảnh, giúp giữ huấn luyện ổn định và tiết kiệm bộ nhớ.

from PIL import Image


TASK = "components"  # "components" | "yaml" | "json" | "schematic"
MAX_TARGET_CHARS = 5000  # safety cap for long targets like schematic/json
MAX_IMAGE_SIDE = 1024          # bigger side
MAX_IMAGE_PIXELS = 1024 * 1024 # safety cap (1.0 MP). raise to 1.5MP if stable

Xây dựng prompt

Hàm build_prompt() tạo văn bản hướng dẫn truyền vào mô hình. Nó sử dụng siêu dữ liệu tập dữ liệu để tạo ngữ cảnh và áp đặt ràng buộc đầu ra chặt chẽ nhằm giảm ảo giác và giữ giám sát nhất quán giữa các tác vụ.

def build_prompt(example):
    # Use dataset fields to give better context (name/type are helpful)
    name = example.get("name") or "Unknown project"
    ftype = example.get("type") or "unknown format"

    if TASK == "components":
        return (
            f"Project: {name}\nFormat: {ftype}\n"
            "From the schematic image, extract all component labels and identifiers exactly as shown "
            "(part numbers, values, footprints, net labels like +5V/GND).\n"
            "Output only a comma-separated list. Do not generalize or add extra text."
        )

    if TASK == "yaml":
        return (
            f"Project: {name}\nFormat: {ftype}\n"
            "From the schematic image, produce YAML metadata for the design.\n"
            "Return valid YAML only. No markdown, no explanations."
        )

    if TASK == "json":
        return (
            f"Project: {name}\nFormat: {ftype}\n"
            "From the schematic image, produce a JSON representation of the schematic structure.\n"
            "Return valid JSON only. No markdown, no explanations."
        )

    if TASK == "schematic":
        return (
            f"Project: {name}\nFormat: {ftype}\n"
            "From the schematic image, reconstruct the raw KiCad schematic content.\n"
            "Return only the schematic text. No markdown, no explanations."
        )

    raise ValueError("Unknown TASK")

Xây dựng mục tiêu

Hàm build_target() trích xuất đầu ra ground‑truth cho tác vụ đã chọn trực tiếp từ tập dữ liệu. Nội dung được trả về nguyên văn để huấn luyện mô hình tái tạo chính xác, không diễn đạt lại.

def build_target(example):
    if TASK == "components":
        comps = example.get("components_used") or []
        return ", ".join(comps)

    if TASK == "yaml":
        return (example.get("yaml") or "").strip()

    if TASK == "json":
        return (example.get("json") or "").strip()

    if TASK == "schematic":
        return (example.get("schematic") or "").strip()

    raise ValueError("Unknown TASK")

Hàm clamp_text() áp dụng giới hạn ký tự cứng cho mục tiêu. Điều này ngăn JSON, YAML hoặc tệp sơ đồ quá lớn gây vấn đề bộ nhớ khi huấn luyện.

def clamp_text(s: str, max_chars: int = MAX_TARGET_CHARS) -> str:
    s = (s or "").strip()
    return s if len(s) <= max_chars else s[:max_chars].rstrip()

Thay đổi kích thước ảnh

Hàm _resize_pil() chuẩn hóa và thay đổi kích thước ảnh sơ đồ trước khi xử lý. Nó áp đặt cả giới hạn cạnh tối đa và tổng số pixel tối đa, đảm bảo việc dùng bộ nhớ GPU có thể dự đoán được trong khi vẫn giữ chi tiết hình ảnh.

def _resize_pil(pil: Image.Image, max_side: int = MAX_IMAGE_SIDE, max_pixels: int = MAX_IMAGE_PIXELS) -> Image.Image:
    pil = pil.convert("RGB")
    w, h = pil.size

    # Scale down if max side too large
    scale_side = min(1.0, max_side / float(max(w, h)))

    # Scale down if too many pixels (area cap)
    scale_area = (max_pixels / float(w * h)) ** 0.5 if (w * h) > max_pixels else 1.0

    scale = min(scale_side, scale_area)

    if scale < 1.0:
        nw, nh = max(1, int(w * scale)), max(1, int(h * scale))
        pil = pil.resize((nw, nh), resample=Image.BICUBIC)

    return pil

7. Thiết lập mẫu chat đa phương thức

Ở bước này, ta chuyển mỗi mẫu đã làm sạch thành định dạng chat đa phương thức có thể được mô hình ngôn ngữ–thị giác Qwen xử lý trực tiếp. Định dạng này gắn rõ ràng ảnh sơ đồ với hướng dẫn văn bản và đầu ra mục tiêu tương ứng.

def to_messages(example):
    prompt = build_prompt(example)
    target = clamp_text(build_target(example))

    example["messages"] = [
        {
            "role": "user",
            "content": [
                {"type": "image"},
                {"type": "text", "text": prompt},
            ],
        },
        {
            "role": "assistant",
            "content": [{"type": "text", "text": target}],
        },
    ]
    return example

Chúng ta xáo trộn tập dữ liệu để loại bỏ thiên lệch thứ tự và chọn một tập con nhỏ cho thử nghiệm ban đầu. 

Sau đó, map tập dữ liệu qua to_messages() để tạo ví dụ huấn luyện đa phương thức. Cuối cùng, bật lại giải mã ảnh để ảnh chỉ được tải khi huấn luyện, giữ tiền xử lý gọn nhẹ và tiết kiệm bộ nhớ.

# Start small (increase later)
train_ds = ds_clean.shuffle(seed=42).select(range(min(800, len(ds_clean)))).map(to_messages)
train_ds = train_ds.cast_column("image", HFImage(decode=True))

8. Đánh giá Qwen3‑VL‑8B (trước fine-tuning)

Trước khi fine-tune, ta đánh giá hiệu năng gốc của mô hình Qwen3‑VL 8B Instruct trên tác vụ này. Mốc cơ sở này giúp hiểu mô hình pretrained có thể trích xuất thông tin từ ảnh sơ đồ tốt đến đâu khi chưa thích nghi đặc thù tác vụ.

Hàm run_inference() thực hiện một lượt suy luận trên một ví dụ, dùng cùng logic prompt và tiền xử lý ảnh như khi huấn luyện sau này.

import torch

def run_inference(model_, example, max_new_tokens=256):
    prompt = build_prompt(example)

    messages = [
        {
            "role": "user",
            "content": [
                {"type": "image", "image": _resize_pil(example["image"])},
                {"type": "text", "text": prompt},
            ],
        }
    ]

    inputs = processor.apply_chat_template(
        messages,
        tokenize=True,
        add_generation_prompt=True,
        return_dict=True,
        return_tensors="pt",
    ).to(model_.device)

    with torch.inference_mode():
        out = model_.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)

    gen = out[0][inputs["input_ids"].shape[1]:]
    return processor.decode(gen, skip_special_tokens=True)

baseline_ex = train_ds.shuffle(seed=120).select(range(1))[0]

Trước tiên, đánh giá mô hình trên một mẫu ngẫu nhiên từ tập huấn luyện.

print("\n--- BASELINE OUTPUT ---\n", run_inference(model, baseline_ex))
print("\n--- TARGET (dataset) ---\n", clamp_text(build_target(baseline_ex), 1500))
--- BASELINE OUTPUT ---
 J1,Conn_02x11_Odd_Even,CINT6,CINT5,CINT4,CINT3,CINT2,CINT1,CINT0,CINT15,CINT14,CINT13,CINT12,CINT11,CINT10,CINT9,CINT8,CINT7,CINT16,CINT17,CINT18,CINT19,CINT20,CINT21,CINT22,CINT23,CINT24,CINT25,CINT26,CINT27,CINT28,CINT29,CINT30,CINT31,CINT32,CINT33,CINT34,CINT35,CINT36,CINT37,CINT38,CINT39,CINT40,CINT41,CINT42,CINT43,CINT44,CINT45,CINT46,CINT47,CINT48,CINT49,CINT50,CINT51,CINT52,CINT53,CINT54,CINT55,CINT56,CINT57,CINT58,CINT59,CINT60,CINT61,CINT62

--- TARGET (dataset) ---
 Conn_02x11_Odd_Even, R_Pack04, GND

Trong ví dụ này, mô hình nhận diện đúng một số yếu tố cấu trúc nhưng sinh quá mức tên chân và tín hiệu, đồng thời không khôi phục được các định danh linh kiện chính xác như trong dữ liệu. Tôi cũng thấy mẫu hình này lặp lại ở nhiều ví dụ khác khi đánh giá.

Nhìn chung, kết quả cơ sở cho thấy mặc dù mô hình có hiểu biết văn bản và hình ảnh mạnh, nó thiếu căn chỉnh với chú thích linh kiện đặc thù của tập dữ liệu. Hành vi này cho thấy cần fine-tuning để giảm ảo giác đầu ra và cải thiện độ chính xác.

9. Xây dựng data collator ngôn ngữ‑thị giác cho huấn luyện

Phần này định nghĩa data collator tuỳ chỉnh để chuẩn bị các batch kiểu chat đa phương thức cho huấn luyện. Nó chuyển từng ví dụ thành tensor sẵn sàng cho mô hình bằng cách mã hoá chung văn bản và ảnh, đồng thời đảm bảo chỉ tính loss trên phản hồi của assistant.

Collator xây dựng hai phiên bản văn bản chat: bản đầy đủ (prompt và mục tiêu) để mã hoá đầu vào, và bản chỉ có prompt để tính độ dài token của prompt. Dựa trên các độ dài này, toàn bộ token prompt và padding bị mask trong nhãn, vì vậy chỉ đầu ra của assistant đóng góp vào loss. Ảnh được thay đổi kích thước nhất quán và áp dụng độ dài chuỗi tối đa cố định để kiểm soát bộ nhớ.

from typing import List, Dict, Any
import torch

MAX_LEN = 1500

def collate_fn(batch: List[Dict[str, Any]]):
    # 1) Build full chat text (includes assistant answer)
    full_texts = [
        processor.apply_chat_template(
            ex["messages"],
            tokenize=False,
            add_generation_prompt=False,
        )
        for ex in batch
    ]

    # 2) Build prompt-only text (up to user turn; generation prompt on)
    prompt_texts = [
        processor.apply_chat_template(
            ex["messages"][:-1],
            tokenize=False,
            add_generation_prompt=True,
        )
        for ex in batch
    ]

    # 3) Images
    images = [_resize_pil(ex["image"]) for ex in batch]

    # 4) Tokenize full inputs ONCE (text + images)
    enc = processor(
        text=full_texts,
        images=images,
        return_tensors="pt",
        padding=True,
        truncation=True,
        max_length=MAX_LEN,
    )

    input_ids = enc["input_ids"]
    pad_id = processor.tokenizer.pad_token_id

    # 5) Compute prompt lengths with TEXT-ONLY tokenization (much cheaper than text+images)
    prompt_ids = processor.tokenizer(
        prompt_texts,
        return_tensors="pt",
        padding=True,
        truncation=True,
        max_length=MAX_LEN,
        add_special_tokens=False,  # chat template already includes special tokens
    )["input_ids"]

    # Count non-pad tokens in prompt
    prompt_lens = (prompt_ids != pad_id).sum(dim=1)

    # 6) Labels: copy + mask prompt tokens + mask padding
    labels = input_ids.clone()
    bs, seqlen = labels.shape

    for i in range(bs):
        pl = int(prompt_lens[i].item())
        pl = min(pl, seqlen)
        labels[i, :pl] = -100

    # Mask padding positions too
    labels[labels == pad_id] = -100

    # If your processor produces pixel_values / image_grid_thw, keep them
    enc["labels"] = labels
    return enc

Collator này cho phép giám sát hiệu quả và đúng đắn cho fine-tuning ngôn ngữ–thị giác.

10. Cấu hình LoRA để fine-tune Qwen3‑VL‑8B hiệu quả

Giờ chúng ta cấu hình LoRA (Low‑Rank Adaptation) để fine-tune mô hình Qwen3‑VL một cách hiệu quả mà không cần cập nhật toàn bộ trọng số. LoRA chèn các ma trận hạng thấp có thể huấn luyện vào những lớp chiếu được chọn, giảm đáng kể việc dùng bộ nhớ trong khi vẫn giữ hiệu năng.

from peft import LoraConfig, TaskType, get_peft_model

lora = LoraConfig(
    r=16,
    lora_alpha=32,
    lora_dropdown=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM,
    target_modules=[
        "q_proj","k_proj","v_proj","o_proj",
        "gate_proj","up_proj","down_proj"
    ],
)

Sau đó, ta định nghĩa cấu hình huấn luyện bằng SFTConfig, đặt batch size, learning rate, độ chính xác và tuỳ chọn logging phù hợp để fine-tuning ổn định trên A100.

from trl import SFTTrainer, SFTConfig

args = SFTConfig(
    output_dir=f"qwen3vl-open-schematics-{TASK}-lora",
    num_train_epochs=1,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,
    gradient_checkpointing=False,
    learning_rate=1e-4,             
    warmup_steps=10,
    weight_decay=0.01,
    max_grad_norm=1.0,
    bf16=True,
    fp16=False,
    lr_scheduler_type="cosine",
    logging_steps=10,
    report_to="none",
    remove_unused_columns=False,
)

Cuối cùng, khởi tạo SFTTrainer, kết hợp mô hình, tập dữ liệu, collator tuỳ chỉnh và cấu hình LoRA để bắt đầu huấn luyện có giám sát.

trainer = SFTTrainer(
    model=model,
    args=args,
    train_dataset=train_ds,
    data_collator=collate_fn,
    peft_config=lora
)

11. Fine-tune Qwen3‑VL‑8B trên bộ dữ liệu Open Schematics

Giờ bắt đầu quá trình fine-tuning bằng trainer đã cấu hình.

trainer.train()

Khi huấn luyện bắt đầu, bạn có thể theo dõi bảng điều khiển telemetry của RunPod. Trên phiên bản A100 80GB, quá trình thường dùng khoảng 40–45 GB VRAM và đạt mức sử dụng GPU gần tối đa, cho thấy tận dụng phần cứng hiệu quả.

Telemetry Runpod trong khi huấn luyện mô hình

Khi huấn luyện diễn ra, bạn sẽ thấy loss huấn luyện giảm dần trước khi ổn định. Trong trường hợp của chúng tôi, loss hội tụ và đi ngang khoảng 6,5, như một chỉ báo cơ bản rằng mô hình đã thích nghi với nhiệm vụ trích xuất linh kiện từ sơ đồ.

Loss huấn luyện giảm dần

Tại thời điểm này, các adapter LoRA đã được fine-tune thành công và sẵn sàng để đánh giá và xuất bản.

12. Xuất bản mô hình đã fine-tune lên Hugging Face Hub

Sau khi hoàn tất fine-tuning, trước tiên ta lưu các adapter LoRA đã huấn luyện và processor liên quan về máy.

out_dir = trainer.args.output_dir  # from your SFTConfig/TrainingArguments

trainer.save_model(out_dir)  # saves model/adapters into output_dir
processor.save_pretrained(out_dir)  # save processor (tokenizer + image processor)

Tiếp theo, xuất bản mô hình đã fine-tune lên Hugging Face Hub. Điều này cho phép tái sử dụng adapter và processor cho suy luận hoặc fine-tuning tiếp.

import os

repo_id = "kingabzpro/qwen3vl-open-schematics-lora" # replace with your username/repo

# Push model/adapters
trainer.model.push_to_hub(
    repo_id,
    token=os.getenv("HF_TOKEN"),
)

# Push processor
processor.push_to_hub(
    repo_id,
    token=os.getenv("HF_TOKEN"),
)

Đẩy adapter mô hình và processor lên Hugging Face Hub

Khi tải lên hoàn tất, các adapter LoRA đã fine-tune và processor sẽ công khai trong kho lưu trữ được chỉ định.

Đã đẩy LoRA fine-tune lên Hugging Face Hub

12. Đánh giá mô hình Qwen3‑VL‑8B đã fine-tune

Sau khi fine-tune, ta tải lại mô hình và processor trực tiếp từ Hugging Face Hub. Điều này đảm bảo việc đánh giá được thực hiện bằng các adapter LoRA đã xuất bản, đúng như cách chúng được dùng trong suy luận thực tế.

model = Qwen3VLForConditionalGeneration.from_pretrained(
    repo_id,
    dtype=torch.bfloat16,
    device_map="auto",
    attn_implementation="flash_attention_2",
)
processor = AutoProcessor.from_pretrained(repo_id)
```
We now repeat the same inference procedure used during the baseline evaluation, allowing for a direct comparison between pre-fine-tuning and post-fine-tuning behavior.
```python
baseline_ex = train_ds.shuffle(seed=120).select(range(1))[0]
print("\n--- FINETUNED OUTPUT ---\n", run_inference(model, baseline_ex))
print("\n--- TARGET (dataset) ---\n", clamp_text(build_target(baseline_ex), 1500))
--- FINETUNED OUTPUT ---
 Conn_02x11_0dd_Even, P3.3V
--- TARGET (dataset) ---
 Conn_02x11_Odd_Even, R_Pack04, GND

So với đường cơ sở, mô hình đã fine-tune tạo đầu ra ngắn gọn và tập trung hơn nhiều, tránh việc sinh thừa quy mô lớn tên chân và tín hiệu suy diễn. 

Dù dự đoán vẫn chưa đầy đủ và còn lỗi nhỏ, nó cho thấy sự dịch chuyển rõ rệt về phía các định danh linh kiện phù hợp với tập dữ liệu.

Ta sẽ đánh giá ví dụ thứ hai để xác nhận hành vi này.

baseline_ex = train_ds.shuffle(seed=170).select(range(1))[0]
print("\n--- FINETUNED OUTPUT ---\n", run_inference(model, baseline_ex))
print("\n--- TARGET (dataset) ---\n", clamp_text(build_target(baseline_ex), 1500))
--- FINETUNED OUTPUT ---
 ATMEGA328P-PU, +5V, GND, R, C, C16MHz, SERVO_A, SERVO_B, SERVO_C, SERVO_D, SERVO_E, SERVO_F, SERVO_G
--- TARGET (dataset) ---
 +5V, 7.62MM-3P, 7.62MM-3P_1, 7.62MM-3P_2, 7.62MM-3P_3, 7.62MM-3P_4, 7.62MM-3P_5, 7.62MM-3P_6, ATMEGA328P-PU, ATMEGA328P-PU_1, GND, MBB02070C1002FCT00, MBB02070C1002FCT00_1, Unknown_0_-806, X49SD16MSD2SC, Y5P102K2KV16CC0224, Y5P102K2KV16CC0224_1, Y5P102K2KV16CC0224_2

Ở đây, mô hình đã fine-tune nhận diện đúng các thành phần lõi như vi điều khiển và các lưới nguồn, và giảm đáng kể những ảo giác ở mức tín hiệu không liên quan. Tuy vậy, nó vẫn tổng quát hoá một số linh kiện thay vì tái tạo đúng các định danh đặc thù của tập dữ liệu.

Tổng thể, các kết quả này cho thấy fine-tuning đã thành công trong việc triệt tiêu sinh thừa và cải thiện căn chỉnh với trích xuất linh kiện ở cấp độ sơ đồ. Mặc dù có thể tăng độ chính xác với thêm epoch, tập huấn luyện lớn hơn, hoặc ràng buộc đầu ra chặt hơn, hành vi sau fine-tuning thể hiện sự cải thiện rõ rệt và đo lường được so với đường cơ sở.

Nếu bạn gặp vấn đề khi chạy mã trong hướng dẫn này, hãy tham khảo notebook đi kèm.

Một vài suy nghĩ cuối

Mô hình ngôn ngữ–thị giác khác căn bản với mô hình chỉ văn bản, và coi chúng như nhau gần như luôn dẫn đến kết quả kém. 

Nếu không cẩn thận, bạn rất dễ gặp lỗi tràn bộ nhớ ngay cả với batch size bằng một, hoặc fine-tune một mô hình có vẻ đang huấn luyện nhưng thực tế không học được tác vụ. Đây là điều tôi đã rút ra bằng trải nghiệm.

Điều tạo nên khác biệt cuối cùng là chú ý đến những chi tiết quan trọng dành riêng cho huấn luyện đa phương thức. 

Giới hạn kích thước ảnh trong ngưỡng an toàn, làm sạch dữ liệu để loại mẫu hỏng hoặc không dùng được, và đảm bảo chỉ truyền vào mô hình các cặp ảnh‑văn bản hợp lệ là những bước thiết yếu. Bỏ qua bất kỳ bước nào trong số này sẽ nhanh chóng dẫn đến mất ổn định hoặc lãng phí tài nguyên tính toán.

Ở phía mô hình, chỉ dùng các lớp mục tiêu LoRA liên quan giúp giữ huấn luyện hiệu quả và tập trung, trong khi tinh chỉnh cẩn thận các tham số huấn luyện cải thiện hội tụ mà không tăng áp lực bộ nhớ. 

Tối ưu cho GPU A100, bật Flash Attention và dùng bfloat16 giúp huấn luyện ổn định đồng thời giảm đáng kể thời gian chạy. Trên thực tế, các tối ưu này rút ngắn gần một nửa thời gian huấn luyện mà không đánh đổi chất lượng.

Kết quả cuối cùng cho thấy ngay cả một mô hình ngôn ngữ–thị giác pretrained mạnh cũng được hưởng lợi rất nhiều từ fine-tuning theo miền. Với tiền xử lý phù hợp, cấu hình đúng và tối ưu hóa theo phần cứng, có thể thích nghi các mô hình đa phương thức lớn một cách tin cậy và hiệu quả.

Nếu bạn muốn luyện tập thêm về fine-tuning, tôi khuyến nghị khóa học Fine‑Tuning with Llama 3.

Abid Ali Awan

Tác giả

Abid Ali Awan

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ủ đề

Liên quan