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

Tinh chỉnh Ministral 3: Hướng dẫn từng bước huấn luyện mô hình ngôn ngữ–thị giác cho phân loại X-quang

Tìm hiểu cách tinh chỉnh mô hình ngôn ngữ–thị giác Ministral 3 cho phân loại ảnh X-quang bằng LoRA, công cụ Hugging Face, prompt tùy chỉnh và TRL.
· 10 phút đọc

Khám phá cùng AI

Ministral 3 là họ mô hình đa phương thức tối ưu cho thiết bị biên, có các kích thước 3B, 8B và 14B, mỗi kích cỡ đều cung cấp biến thể Base, Instruct và Reasoning với hỗ trợ thị giác đầy đủ. 

Được thiết kế để mang lại hiệu năng tiệm cận nhóm dẫn đầu trong khi vẫn đủ gọn nhẹ cho triển khai cục bộ và riêng tư, Ministral 3 cân bằng giữa năng lực tiên tiến và ràng buộc phần cứng thực tế; mẫu chủ lực 14B Instruct chạy vừa 24GB VRAM ở FP8 và có thể sánh với các hệ lớn hơn như Mistral Small 3.2 24B. Mô hình có cửa sổ ngữ cảnh lớn 256k, bao phủ đa ngôn ngữ mạnh, gọi hàm gốc, tuân thủ system prompt tốt, và giấy phép mở Apache 2.0. 

Trong hướng dẫn này, chúng ta sẽ tinh chỉnh mô hình Ministral 3 14B Instruct trên một tập dữ liệu X-quang gãy xương để cải thiện khả năng phát hiện và phân loại chính xác các nhóm gãy.

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

  1. Tải tập dữ liệu từ Kaggle.
  2. Chuyển đổi sang tập dữ liệu Hugging Face.
  3. Tạo prompt huấn luyện rõ ràng cho phân loại X-quang.
  4. Thiết lập pipeline huấn luyện ngôn ngữ–thị giác với Ministral 3 14B.
  5. Tinh chỉnh mô hình và lưu checkpoint.
  6. Tải adapter mô hình đã huấn luyện để suy luận.
  7. So sánh kết quả trước và sau tinh chỉnh.

Nếu bạn muốn tìm hiểu thêm về các mô hình của Mistral AI, hãy tham khảo hướng dẫn Mistral 3 và hướng dẫn Mistral 3 Large. 

Lưu ý: Hướng dẫn này minh họa quy trình tinh chỉnh kỹ thuật nhằm mục đích giáo dục. Mô hình tạo ra không dành cho mục đích y tế hay lâm sàng và không nên được sử dụng để chẩn đoán, ra quyết định hay chăm sóc bệnh nhân.

1. Thiết lập môi trường để tinh chỉnh mô hình ngôn ngữ–thị giác Ministral 3

Dù chúng ta tinh chỉnh mô hình 14B, huấn luyện ngôn ngữ–thị giác đòi hỏi bộ nhớ nhiều hơn đáng kể so với tinh chỉnh chỉ văn bản. Bộ mã hóa ảnh làm tăng tải GPU, nghĩa là chúng ta cần GPU A100 với 80 GB VRAM để chạy mượt mà.

Trong dự án này, tôi dùng Runpod và khởi chạy một pod A100.

RunPod. A100 Pod.

Sau khi khởi chạy pod, tăng dung lượng lưu trữ container lên 50 GB. Sau đó thêm biến môi trường cho cả khóa API của Kaggle và Hugging Face. 

Kaggle cho phép tải trực tiếp tập dữ liệu X-quang xương, còn Hugging Face cho phép đẩy adapter LoRA đã tinh chỉnh lên hub mô hình.

Thiết lập biến môi trường trong Runpod

Tiếp theo, mở JupyterLab và tạo một notebook mới. Trước khi huấn luyện, chúng ta cần cài đặt tất cả các gói Python cần thiết. 

Hãy đảm bảo thư viện Transformers được cập nhật lên phiên bản mới nhất, vì hỗ trợ Ministral 3 mới được bổ sung gần đây; các bản cũ sẽ không tải mô hình đúng cách.

! pip install --upgrade --quiet bitsandbytes datasets evaluate hf_transfer peft scikit-learn matplotlib Pillow kaggle ipywidgets
!pip install --quiet git+https://github.com/huggingface/transformers.git@bf3f0ae70d0e902efab4b8517fce88f6697636ce
!pip install --quiet --no-deps trl==0.22.2

Khi đã cài đặt xong, bạn sẵn sàng tải dữ liệu, chuẩn bị collator và bắt đầu tinh chỉnh.

2. Tải tập dữ liệu gãy xương từ Kaggle

Ở bước tiếp theo, chúng ta tạo thư mục gốc cho tập dữ liệu và dùng Kaggle Python API để tải bộ Bone Break Classification Image. 

Bộ dữ liệu này chứa nhiều loại gãy xương ở định dạng X-quang và sẽ dùng để tinh chỉnh Ministral 3 cho nhiệm vụ phân loại hình ảnh.

import os
import glob
from kaggle.api.kaggle_api_extended import KaggleApi

DATA_ROOT = "/bone-break"
os.makedirs(DATA_ROOT, exist_ok=True)

kaggle_dataset = "pkdarabi/bone-break-classification-image-dataset"

# Initialize and authenticate Kaggle API (it will read KAGGLE_USERNAME / KAGGLE_KEY)
api = KaggleApi()
api.authenticate()

# Download and unzip the dataset using the Python API
api.dataset_download_files(
    kaggle_dataset,
    path=DATA_ROOT,
    unzip=True,
)

Bạn có thể xem tập dữ liệu trên Kaggle tại đây:

Dataset URL: https://www.kaggle.com/datasets/pkdarabi/bone-break-classification-image-dataset

3. Xây dựng tập dữ liệu Hugging Face

Tập dữ liệu đã tải không được tổ chức theo cấu trúc thông thường. Mỗi nhóm gãy xương chứa cả thư mục train và test ở bên trong. 

Vì vậy trước tiên, ta cần phương thức duyệt qua từng thư mục lớp, tách hai phần train/test, rồi tạo một tập dữ liệu Hugging Face kết hợp với ánh xạ train và test gọn gàng.

import glob, os
from typing import Any, Dict, List

from datasets import Dataset, DatasetDict, Features, ClassLabel, Image

inner_root = os.path.join(
    DATA_ROOT, "Bone Break Classification", "Bone Break Classification"
)

classes = []
train_records: List[Dict[str, Any]] = []
test_records: List[Dict[str, Any]] = []

for class_name in sorted(os.listdir(inner_root)):
    class_dir = os.path.join(inner_root, class_name)
    if not os.path.isdir(class_dir):
        continue

    classes.append(class_name)

    for split_name, split_key in [("Train", "train"), ("Test", "test")]:
        split_dir = os.path.join(class_dir, split_name)
        if not os.path.isdir(split_dir):
            continue

        for ext in ("*.png", "*.jpg", "*.jpeg"):
            for img_path in glob.glob(os.path.join(split_dir, ext)):
                rec = {"image": img_path, "label_name": class_name}
                if split_key == "train":
                    train_records.append(rec)
                else:
                    test_records.append(rec)

print("Classes:", classes)
print("Train samples:", len(train_records))
print("Test samples:", len(test_records))

Chúng ta có tổng cộng 10 loại phân loại, với 989 mẫu huấn luyện và 140 mẫu kiểm tra.

Classes: ['Avulsion fracture', 'Comminuted fracture', 'Fracture Dislocation', 'Greenstick fracture', 'Hairline Fracture', 'Impacted fracture', 'Longitudinal fracture', 'Oblique fracture', 'Pathological fracture', 'Spiral Fracture']
Train samples: 989
Test samples: 140

Giờ chúng ta sẽ mã hóa nhãn cho tập dữ liệu Hugging Face để mỗi lớp nhãn được coi là nhãn phân loại và mỗi ảnh là đặc trưng ảnh. 

Chúng ta sẽ tạo hai tập: một cho huấn luyện và một cho kiểm tra. Cuối cùng, gộp chúng thành một tập duy nhất chứa cả hai phần.

# Encode labels
classes = sorted(list(set(classes)))
label2id = {name: i for i, name in enumerate(classes)}

for rec in train_records:
    rec["label"] = label2id[rec["label_name"]]
for rec in test_records:
    rec["label"] = label2id[rec["label_name"]]

features = Features(
    {
        "image": Image(),
        "label": ClassLabel(names=classes),
    }
)

train_ds = Dataset.from_list(
    [{"image": r["image"], "label": r["label"]} for r in train_records],
    features=features,
)
test_ds = Dataset.from_list(
    [{"image": r["image"], "label": r["label"]} for r in test_records],
    features=features,
)

data = DatasetDict({"train": train_ds, "test": test_ds})
data

Tổng quan tập dữ liệu như sau. 

DatasetDict({
    train: Dataset({
        features: ['image', 'label'],
        num_rows: 989
    })
    test: Dataset({
        features: ['image', 'label'],
        num_rows: 140
    })
})

4. Khám phá phân bố lớp và xem mẫu ảnh X-quang

Trước khi tinh chỉnh, việc hiểu độ cân bằng của tập dữ liệu và đặc điểm ảnh là hữu ích. Chúng ta sẽ bắt đầu bằng cách kiểm tra số mẫu huấn luyện ở mỗi loại gãy xương.

from collections import Counter

label_names = data["train"].features["label"].names
label_ids = data["train"]["label"]

counts = Counter(label_ids)
print("Training Class counts:")
for lbl_id, cnt in counts.items():
    print(f"{lbl_id:2d} ({label_names[lbl_id]}): {cnt}")

FRACTURE_CLASSES = label_names

Các nhóm tương đối cân bằng, dù tổng số mẫu là nhỏ cho một tác vụ phân loại thị giác phức tạp. 

Trước mắt, chúng ta sẽ tiếp tục với tập này, nhưng cần diễn giải kết quả với kỳ vọng thực tế do số mẫu hạn chế.

Training Class counts:
 0 (Avulsion fracture): 109
 1 (Comminuted fracture): 134
 2 (Fracture Dislocation): 137
 3 (Greenstick fracture): 106
 4 (Hairline Fracture): 101
 5 (Impacted fracture): 75
 6 (Longitudinal fracture): 68
 7 (Oblique fracture): 69
 8 (Pathological fracture): 116
 9 (Spiral Fracture): 74

Tiếp theo, hãy xem vài ảnh ngẫu nhiên để hiểu trực quan về tập dữ liệu.

import matplotlib.pyplot as plt
from datasets import Image

# Cast the 'image' column to Hugging Face Image feature
dataset_images = data.cast_column("image", Image())

# Select random samples from the training set
samples = dataset_images["train"].shuffle(seed=420).select(range(4))

# Plot
fig, axes = plt.subplots(1, 4, figsize=(16, 5))

for i, sample in enumerate(samples):
    img_object = sample["image"]              # PIL Image
    label_id = sample["label"]               # int
    label_name = label_names[label_id]       # string

    axes[i].imshow(img_object)
    axes[i].set_title(f"Label: {label_name}")
    axes[i].axis("off")

plt.tight_layout()
plt.show()

Quan sát các ảnh mẫu, có thể thấy sự khác biệt về kích thước, độ phân giải, hướng và chất lượng quét. Điều này không ảnh hưởng đến việc tinh chỉnh mô hình ngôn ngữ–thị giác, nhưng có thể tác động đến mức độ mô hình học nhất quán từng loại gãy. 

Việc phát hiện gãy xương bằng quan sát có thể khá rõ ràng, nhưng phân loại vào một trong mười loại gãy y khoa tinh vi đòi hỏi dữ liệu có cấu trúc hơn so với những gì có ở đây.

4 random sample from bone fracture dataset

5. Thiết kế prompt cho phân loại gãy xương

Tiếp theo, chúng ta cần quyết định cách mô hình sẽ ứng xử. Với tác vụ này, mục tiêu rất nghiêm ngặt: khi nhận một ảnh X-quang, mô hình phải xuất ra chính xác một tên lớp gãy xương và không có gì khác. Không giải thích, không markdown, không văn bản thừa.

Chúng ta sẽ viết một prompt chỉ dẫn rõ ràng rồi dùng nó để xây dựng cột messages trong tập dữ liệu. 

PROMPT = f"""
You are a radiology assistant specialised in bone fracture classification.

You must classify the X-ray image into exactly ONE of the following classes:
{", ".join(FRACTURE_CLASSES)}

Rules:
- Reply with ONLY the class name.
- No explanations, no extra words, no markdown, no punctuation.
- Output must be EXACTLY one of: {", ".join(FRACTURE_CLASSES)}
""".strip()


from typing import Dict, Any

def convert_to_conversation(example: Dict[str, Any]) -> Dict[str, Any]:
    conversation = [
        {
            "role": "user",
            "content": [
                {"type": "text",  "text": PROMPT},
                {"type": "image"},     # no image bytes here
            ],
        },
        {
            "role": "assistant",
            "content": [
                {"type": "text", "text": FRACTURE_CLASSES[example["label"]]},
            ],
        },
    ]
    example["messages"] = conversation
    return example




formatted_data = data["train"].map(convert_to_conversation)
formatted_data["messages"][0]

Điều này tạo ra cột messages trong đó mỗi bản ghi chứa một hội thoại nhỏ hoàn chỉnh: chỉ dẫn và lớp gãy xương đúng. Đây chính là những gì mô hình ngôn ngữ–thị giác sẽ thấy trong quá trình tinh chỉnh.

Dataset in conversational style.

6. Tải mô hình ngôn ngữ–thị giác Ministral 3

Giờ chúng ta tải phiên bản BF16 của mô hình Ministral-3 14B Instruct từ Hugging Face Hub, kèm theo processor. Processor gộp tokenizer và pipeline tiền xử lý ảnh, vốn cần thiết cho đầu vào ngôn ngữ–thị giác.

import torch
from transformers import AutoProcessor, AutoModelForImageTextToText, BitsAndBytesConfig

model_id = "mistralai/Ministral-3-14B-Instruct-2512-BF16"


model_kwargs = dict(
    attn_implementation="eager",
    torch_dtype=torch.bfloat16,
    device_map="auto",
)

model = AutoModelForImageTextToText.from_pretrained(model_id, **model_kwargs)
processor = AutoProcessor.from_pretrained(model_id)

# Use right padding to avoid issues during training
processor.tokenizer.padding_side = "right"

7. Thiết lập mẫu chat tùy chỉnh

Mẫu chat gốc do Mistral AI cung cấp được thiết kế cho các trường hợp hội thoại dài. Với dự án này, đó lại thành vấn đề. Mô hình bắt đầu tập trung vào các chỉ dẫn vai trò và văn bản hệ thống dài, bỏ qua mục tiêu thực sự là dự đoán một lớp gãy xương duy nhất từ ảnh X-quang.

Để khắc phục, chúng ta định nghĩa một mẫu ngôn ngữ–thị giác ngắn hơn, chỉ giữ phần quan trọng: chỉ dẫn của người dùng và câu trả lời của trợ lý. Điều này giảm nhiễu và giúp mô hình học ánh xạ sạch từ ảnh sang nhãn lớp.

SHORT_VLM_TEMPLATE = r"""{{ bos_token }}[INST]
{% for message in messages if message['role'] == 'user' -%}
  {%- for content in message['content'] -%}
    {%- if content['type'] == 'text' -%}
{{ content['text'] }}

    {%- elif content['type'] == 'image' -%}
[IMG]
    {%- endif -%}
  {%- endfor -%}
{%- endfor -%}
[/INST]{% if not add_generation_prompt %}
  {%- for message in messages if message['role'] == 'assistant' -%}
{{ message['content'][0]['text'] }}{{ eos_token }}
  {%- endfor -%}
{%- endif %}"""


# Attach to processor / tokenizer
processor.chat_template = SHORT_VLM_TEMPLATE
processor.tokenizer.chat_template = SHORT_VLM_TEMPLATE

8. Kiểm thử Ministral 3 (trước tinh chỉnh)

Trước khi bắt đầu tinh chỉnh, việc kiểm thử mô hình Ministral 3 14B Instruct gốc để tạo đường cơ sở là hữu ích. 

Đầu tiên, ta đưa cho mô hình một ảnh ngẫu nhiên từ tập dữ liệu và hỏi một câu chung chung, chỉ để xem nó hiểu nội dung X-quang như thế nào.

image = formatted_data[200]["image"]

messages = [
    {"role": "user", "content": [
        {"type": "text", "text": "What is the image about?"},
        {"type": "image"},
        
    ]}
]

input_text = processor.apply_chat_template(messages, add_generation_prompt = True)
inputs = processor(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to(device="cuda", dtype=torch.bfloat16)

from transformers import TextStreamer
text_streamer = TextStreamer(processor, skip_prompt = True)
_ = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 1000,
                   use_cache = True, temperature = 1.5, min_p = 0.1)

Kết quả là mô hình hiểu nội dung tổng quát của X-quang, nhưng sau đó trôi sang các mô tả không đáng tin cậy cho mục đích y khoa.

Tiếp theo, ta thử điều gần với mục tiêu cuối cùng hơn. Vẫn dùng ảnh đó, nhưng lần này dùng prompt phân loại gãy xương nghiêm ngặt thay vì câu hỏi mở.

image = formatted_data[200]["image"]

messages = [
    {"role": "user", "content": [
        {"type": "text", "text": PROMPT},
        {"type": "image"},
        
    ]}
]

input_text = processor.apply_chat_template(messages, add_generation_prompt = True)
inputs = processor(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to(device="cuda", dtype=torch.bfloat16)

from transformers import TextStreamer
text_streamer = TextStreamer(processor, skip_prompt = True)
_ = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 1000,
                   use_cache = True, temperature = 1.5, min_p = 0.1

Với mẫu này, mô hình trả lời:

Hairline Fracture</s>

Giờ kiểm tra nhãn sự thật cho cùng ảnh đó.

FRACTURE_CLASSES[formatted_data[200]["label"]]

Kết quả trả về:

'Comminuted fracture'

Vậy là mô hình làm đúng định dạng chỉ dẫn và sinh ra một tên lớp duy nhất, nhưng bản thân phân loại lại sai. 

Đây chính là lý do cần tinh chỉnh. Mô hình gốc có khả năng hiểu thị giác mạnh, nhưng chưa được huấn luyện đặc thù để phân biệt các loại gãy xương chi tiết.

Ở các bước tiếp theo, chúng ta sẽ tinh chỉnh trên tập gãy xương để cải thiện hành vi đó.

9. Cấu hình LoRA để tinh chỉnh Ministral 3 hiệu quả

Tiếp theo, ta thiết lập LoRA để tinh chỉnh Ministral 3 một cách hiệu quả mà không cần cập nhật toàn bộ tham số. Cấu hình này nhắm vào các lớp attention chính và MLP projection được dùng trong mô hình ngôn ngữ–thị giác kiểu Mistral.

from peft import LoraConfig, TaskType, get_peft_model

# LoRA config for a Mistral-style VLM used as a causal LM
peft_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    bias="none",
    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj",
        "gate_proj",
        "up_proj",
        "down_proj",
    ],
    modules_to_save=None,
)

Tiếp theo, chúng ta định nghĩa các tham số huấn luyện SFT (supervised fine-tuning).

Cấu hình này kiểm soát toàn bộ quá trình huấn luyện, bao gồm cách xử lý batch, tối ưu bộ nhớ, hành vi learning rate, logging, chiến lược lưu, và tích hợp với Hugging Face Hub. 

from trl import SFTConfig

args = SFTConfig(
    output_dir="ministral-3-bone-fracture",            
    num_train_epochs=1,                       
    per_device_train_batch_size=2,                           
    per_device_eval_batch_size=2,                            
    gradient_accumulation_steps=4,                           
    gradient_checkpointing=True,                             
    optim="adamw_8bit",                               
    logging_steps=0.1,                                        
    save_strategy="epoch",                                                                                                             
    learning_rate=2e-4,                             
    bf16=True,
    fp16=False,
    max_grad_norm=0.3,                                       
    warmup_steps=25,                                       
    lr_scheduler_type="linear",                              
    push_to_hub=True,                                        
    report_to="none",
    gradient_checkpointing_kwargs={"use_reentrant": False},  
    dataset_kwargs={"skip_prepare_dataset": True},           
    remove_unused_columns = False,                           
    label_names=["labels"],                                  
)

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

Để tinh chỉnh một mô hình ngôn ngữ–thị giác như Ministral 3, chúng ta cần một data collator tùy chỉnh có thể gom batch ảnh và văn bản dạng hội thoại theo định dạng nhất quán. 

Collator dưới đây đảm nhiệm: thiết lập tokenizer và pad token, áp dụng mẫu chat, tokenize văn bản và ảnh cùng nhau, giới hạn độ dài chuỗi an toàn, và tạo nhãn trong đó padding và image token được che (-100) để không ảnh hưởng tới loss. 

from typing import Any, Dict, List
import torch

# --- One-time processor setup ---
tokenizer = processor.tokenizer
tokenizer.padding_side = "right"

# Ensure we have a pad token
if tokenizer.pad_token is None:
    tokenizer.add_special_tokens({"pad_token": tokenizer.eos_token})

pad_token_id = tokenizer.pad_token_id
image_token_id = getattr(processor, "image_token_id", None)

# Hard cap for sequence length
MAX_SEQ_LEN = 2048 


def collate_fn(examples: List[Dict[str, Any]]) -> Dict[str, torch.Tensor]:
    """
    Collate function for VLM fine-tuning.

    Each example must contain:
      - "image": a PIL image
      - "messages": chat-style conversation (list[dict])
    """
    images = [ex["image"] for ex in examples]
    conversations = [ex["messages"] for ex in examples]

    # Build chat prompts for the whole batch
    chat_texts = processor.apply_chat_template(
        conversations,
        add_generation_prompt=False,
        tokenize=False,
    )

    # Tokenize + process images
    batch = processor(
        text=chat_texts,
        images=images,
        padding="longest", 
        truncation=True,
        max_length=MAX_SEQ_LEN,
        return_tensors="pt",
    )

    # Labels: same as input_ids but mask pad & image tokens
    labels = batch["input_ids"].clone()

    if pad_token_id is not None:
        labels[labels == pad_token_id] = -100

    if image_token_id is not None and image_token_id != pad_token_id:
        labels[labels == image_token_id] = -100

    batch["labels"] = labels
    return batch

11. Tinh chỉnh Ministral 3 bằng TRL

Với LoRA adapter, cấu hình huấn luyện và data collator đã sẵn sàng, giờ ta có thể nối tất cả vào SFTTrainer của trl. 

Trainer này xử lý trọn vòng lặp tinh chỉnh có giám sát cho mô hình ngôn ngữ–thị giác Ministral 3 của chúng ta, sử dụng tập dữ liệu đã định dạng, cấu hình LoRA và hàm collate tùy chỉnh cho batch ảnh–văn bản.

from trl import SFTTrainer

trainer = SFTTrainer(
    model=model,
    args=args,
    train_dataset=formatted_data,
    peft_config=peft_config,
    processing_class=processor,
    data_collator=collate_fn,
)

Chạy đoạn mã dưới đây để bắt đầu quá trình huấn luyện. 

trainer.train()

Trong lần chạy của tôi, tinh chỉnh sử dụng khoảng 57 GB VRAM, và nhiều hơn một chút trong các bước đánh giá chạy sau vài vòng lặp. Thực tế, bạn có thể tinh chỉnh cấu hình này thoải mái trên phần cứng dưới 70 GB VRAM.

Runpod telemetry during the fine-tuning of the Ministral 3 14B model.

Toàn bộ huấn luyện mất khoảng nửa giờ, và training loss giảm đều theo thời gian, đây là dấu hiệu tốt cho thấy mô hình đang học từ tập dữ liệu.

Traning loss of the fine-tuning of the Ministral 3 14B model

Khi hoàn tất huấn luyện, chúng ta lưu mô hình đã thích ứng lên Hugging Face Hub. Điều này sẽ tạo một kho mới và đẩy tất cả tệp cần thiết để bạn có thể tải lại và chạy mô hình đã tinh chỉnh ở bất kỳ môi trường nào.

trainer.save_model()

Bạn có thể xem mô hình tại kingabzpro/ministral-3-bone-fracture. 

Saved fine-tuned model on the Hugging Face hub.

12. Đánh giá mô hình Ministral 3 đã tinh chỉnh

Giờ chúng ta tải mô hình và processor đã tinh chỉnh từ Hugging Face Hub. Bên dưới, thao tác này sẽ tải adapter LoRA đã lưu, tải checkpoint Ministral 3 gốc và gộp adapter vào mô hình gốc để sẵn sàng suy luận.

import torch
from transformers import AutoProcessor, AutoModelForImageTextToText, BitsAndBytesConfig

model_id = "kingabzpro/ministral-3-bone-fracture"


model_kwargs = dict(
    attn_implementation="eager",
    dtype=torch.bfloat16,
    device_map="auto",
)

model = AutoModelForImageTextToText.from_pretrained(model_id, **model_kwargs)
processor = AutoProcessor.from_pretrained(model_id)

processor.tokenizer.padding_side = "left"

Để so sánh công bằng, trước hết ta dùng lại cùng ảnh đã kiểm thử trước tinh chỉnh (chỉ số 200) và chạy qua pipeline suy luận với prompt phân loại nghiêm ngặt.

image = formatted_data[200]["image"]

messages = [
    {"role": "user", "content": [
        {"type": "text", "text": PROMPT},
        {"type": "image"},
        
    ]}
]

input_text = processor.apply_chat_template(messages, add_generation_prompt = True)
inputs = processor(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to(device="cuda", dtype=torch.bfloat16)

from transformers import TextStreamer
text_streamer = TextStreamer(processor, skip_prompt = True)
_ = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 100)

Chúng ta đạt kết quả hoàn hảo. Không cần hậu xử lý thêm. Mô hình tuân thủ quy tắc prompt và sinh trực tiếp lớp đúng.

Comminuted fracture</s>

Tiếp theo, thử một mẫu ngẫu nhiên khác từ tập dữ liệu để xác nhận hành vi này không chỉ giới hạn ở một ví dụ.

image = formatted_data[400]["image"]

messages = [
    {"role": "user", "content": [
        {"type": "text", "text": PROMPT},
        {"type": "image"},
        
    ]}
]

input_text = processor.apply_chat_template(messages, add_generation_prompt = True)
inputs = processor(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to(device="cuda", dtype=torch.bfloat16)

from transformers import TextStreamer
text_streamer = TextStreamer(processor, skip_prompt = True)
_ = model.generate(**inputs, streamer = text_streamer, max_new_tokens = 100)

Kết quả mô hình:

Greenstick fracture</s>

Nhãn sự thật:

FRACTURE_CLASSES[formatted_data[400]["label"]]

Kết quả:

'Greenstick fracture'

Một lần nữa, dự đoán chính xác và trùng khớp hoàn toàn với nhãn.

Với các kiểm tra này, có thể nói mô hình Ministral 3 14B của chúng ta đã được tinh chỉnh thành công trên tập X-quang gãy xương và hiện có thể tuân thủ prompt chỉ dẫn, đưa ra dự đoán một lớp chính xác cho các ví dụ này.

Suy ngẫm cuối

Tinh chỉnh mô hình Ministral 3 đòi hỏi kiên nhẫn và điều chỉnh dần dần. Tôi thực sự mất hai ngày để hiểu vì sao mô hình không huấn luyện như kỳ vọng và vì sao, dù tương đối nhỏ, nó vẫn khiến GPU của tôi tràn bộ nhớ. 

Nguyên nhân chính rất đơn giản nhưng dễ bị bỏ qua. Đây không chỉ là mô hình ngôn ngữ. Nó là mô hình ngôn ngữ–thị giác, nghĩa là có bộ mã hóa ảnh làm tăng mức dùng bộ nhớ. 

Độ phân giải ảnh lớn kéo theo mức tiêu thụ VRAM cao hơn trực tiếp. Giảm kích thước ảnh hoặc bắt đầu với batch nhỏ giúp quá trình huấn luyện ổn định hơn nhiều.

Một vấn đề khác tôi gặp phải là dùng mẫu chat mặc định. Nó quá dài và nặng cấu trúc cho kiểu tác vụ này. Mô hình tốn nhiều công sức để phù hợp định dạng mẫu hơn là học cách đọc ảnh và đưa ra phân loại gãy rõ ràng. 

Khi tôi đơn giản hóa prompt thành chỉ dẫn trực tiếp, mô hình cuối cùng bắt đầu phản hồi đúng với đầu vào hình ảnh thay vì định dạng mọi câu trả lời như một phản hồi chat đầy đủ.

Tôi cũng từng cho rằng vì mô hình có khả năng thị giác mạnh nên sẽ tự nhiên làm tốt trên ảnh y khoa. Giả định đó làm tôi chậm lại. Gọi nó là “MedGemma” trong đầu không công bằng với mô hình hay quy trình. Nó không phải mô hình y khoa chuyên biệt ngay từ đầu, và kỳ vọng nó cư xử như vậy mà không được huấn luyện phù hợp là sai lầm của tôi.

Cuối cùng, tôi đã vượt qua từng trở ngại và biến những gì học được thành hướng dẫn này. Mục tiêu là giúp bạn tránh bối rối tương tự và đạt kết quả có ý nghĩa nhanh hơn. 

“Với ảnh nhỏ hơn, prompt đơn giản hơn và kỳ vọng thực tế, Ministral 3 có thể được tinh chỉnh để hiểu X-quang mượt mà và tin cậy hơn nhiều.”

Nếu bạn muốn thực hành tinh chỉnh nhiều hơn, tôi gợi ý 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ủ đề