下一步,我们为数据集设置根文件夹,并使用 Kaggle Python API 下载 Bone Break Classification Image 数据集。
该数据集以 X 光形式包含多种骨折类别,将用于对 Ministral 3 进行视觉分类微调。
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,)技术支持
# Encode labels
classes =sorted(list(set(classes)))
label2id ={name: i for i, name inenumerate(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技术支持
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技术支持
这些类别相对较为平衡,但对于复杂的视觉分类任务来说,总体样本量偏小。
目前我们先用这个数据集继续,但由于样本量有限,解读结果时应保持理性预期。
Training Class counts:0(Avulsion fracture):1091(Comminuted fracture):1342(Fracture Dislocation):1373(Greenstick fracture):1064(Hairline Fracture):1015(Impacted fracture):756(Longitudinal fracture):687(Oblique fracture):698(Pathological fracture):1169(Spiral Fracture):74技术支持
接下来我们随机查看几张图像,以便从视觉上更好地理解数据集。
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 inenumerate(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()技术支持
接下来我们需要确定模型的行为方式。本任务的目标非常严格:给定一张 X 光图像,模型应只输出一个骨折类别名称且不包含任何其他内容。不允许解释、Markdown 或多余文本。
我们将编写清晰的指令提示词,然后用它来在数据集中构建一个 messages 列。
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
defconvert_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]技术支持
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. 设置自定义聊天模板
Mistral AI 提供的原始聊天模板面向长对话场景。在本项目中,这成了问题:模型开始关注冗长的角色指令和系统文本,反而忽视了真正的目标——从 X 光中预测单个骨折类别。
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,)技术支持
随后定义监督微调(SFT)的训练参数。
该配置控制完整训练过程,包括批处理方式、内存优化、学习率策略、日志记录、保存策略,以及与 Hugging Face Hub 的集成。
Ministral 3 是一个面向边缘优化的多模态模型家族,提供 3B、8B 和 14B 三种规模,每种规模均包含 Base、Instruct 和 Reasoning 变体,并全面支持视觉能力。
该系列兼顾强大的前沿性能与轻量化部署需求,适合本地与私有环境。旗舰 14B Instruct 模型在 FP8 下可装入 24GB 显存,性能可与更大型的系统(如 Mistral Small 3.2 24B)媲美。其还具备 256k 超大上下文窗口、稳健的多语言覆盖、原生函数调用、对系统提示的强遵循,以及 Apache 2.0 开源许可。
本教程将把 Ministral 3 14B Instruct 模型在骨折 X 光数据集上进行微调,以提升其检测骨折并准确分类的能力。
您将学到如何:
如果您想进一步了解 Mistral AI 模型,推荐阅读我们的指南Mistral 3及Mistral 3 Large 教程。
免责声明: 本教程仅为教育目的演示技术性微调流程。所得模型不用于任何医疗或临床用途,不得用于诊断、决策或患者护理。
1. Ministral 3 视觉-语言微调的环境搭建
尽管我们微调的是 14B 模型,但视觉-语言训练比纯文本微调需要显著更多的显存。图像编码器会增加 GPU 负载,这意味着我们需要使用 80 GB 显存的 A100 GPU,才能顺利运行该流程。
本项目我使用 Runpod 并启动了一个 A100 pod。
启动 pod 后,将容器存储增至 50 GB。然后为 Kaggle 和 Hugging Face 的 API 密钥添加环境变量。
Kaggle 便于我们直接下载骨骼 X 光数据集,Hugging Face 则用于将微调后的 LoRA 适配器推送回模型中心。
接下来打开 JupyterLab,创建一个新笔记本。训练前需要安装所有必需的 Python 包。
请确保Transformers 库已更新至最新版本,因为 Ministral 3 的支持是近期才加入的,旧版本将无法正确加载模型。
一切安装完毕后,您就可以加载数据集、准备 collator,并开始微调了。
2. 从 Kaggle 下载骨折数据集
下一步,我们为数据集设置根文件夹,并使用 Kaggle Python API 下载 Bone Break Classification Image 数据集。
该数据集以 X 光形式包含多种骨折类别,将用于对 Ministral 3 进行视觉分类微调。
您可以在 Kaggle 上查看该数据集:
3. 构建 Hugging Face 数据集
下载后的数据集并非常见的组织结构。每个骨折类别目录内部同时包含训练和测试文件夹。
因此我们首先需要一种方法,遍历每个类别文件夹,分离两个切分集,然后创建一个合并的 Hugging Face 数据集,包含干净的 train/test 映射。
我们共有 10 个分类,训练样本 989 个,测试样本 140 个。
接下来我们为 Hugging Face 数据集编码标签,使每个标签类别被视为类标签,每张图像被视为图像特征。
我们将创建两个数据集:一个用于训练,一个用于测试。最后将它们合并为同时包含训练与测试子集的单一数据集。
以下是数据集概览。
4. 探索类别分布与样例 X 光图像
在微调之前,了解数据集的平衡性以及图像的样貌会很有帮助。我们先检查每个骨折类别的训练样本数量。
这些类别相对较为平衡,但对于复杂的视觉分类任务来说,总体样本量偏小。
目前我们先用这个数据集继续,但由于样本量有限,解读结果时应保持理性预期。
接下来我们随机查看几张图像,以便从视觉上更好地理解数据集。
从样例图像可见,尺寸、分辨率、方向与扫描质量各不相同。这不会影响视觉-语言模型的微调,但会影响模型对各骨折类别学习的一致性。
直观地检测骨折往往较为直接,但要将其归入十种细微差别的医学骨折类型之一,则需要比此处更结构化的数据。
5. 骨折分类的提示词设计
接下来我们需要确定模型的行为方式。本任务的目标非常严格:给定一张 X 光图像,模型应只输出一个骨折类别名称且不包含任何其他内容。不允许解释、Markdown 或多余文本。
我们将编写清晰的指令提示词,然后用它来在数据集中构建一个
messages列。这会创建一个
messages列,其中每条记录都包含一段完整的微型对话:指令加上正确的骨折类别。这正是视觉-语言模型在微调期间将看到的内容。6. 加载 Ministral 3 视觉-语言模型
现在我们从 Hugging Face Hub 加载 BF16 版本的 Ministral-3 14B Instruct 模型与处理器。处理器将分词器与图像预处理管线打包在一起,适配视觉-语言输入。
7. 设置自定义聊天模板
Mistral AI 提供的原始聊天模板面向长对话场景。在本项目中,这成了问题:模型开始关注冗长的角色指令和系统文本,反而忽视了真正的目标——从 X 光中预测单个骨折类别。
为了解决这个问题,我们定义了一个更简短的视觉-语言模板,仅保留关键部分:用户指令与助手答案。这样可以降低噪声,帮助模型学习从图像到类别标签的干净映射。
8. 微调前测试 Ministral 3
在开始微调前,先测试原始的 Ministral 3 14B Instruct 模型,以便建立基线。
首先我们从数据集中取一张随机图像,提出一个泛化问题,看看它对 X 光内容的理解如何。
可以看到,模型对 X 光的总体内容有所理解,但随后转向了不适用于医疗用途的描述。
接着我们尝试更接近最终目标的设置。仍使用相同图像,但这次采用严格的骨折分类提示词,而非开放式问题。
对于该样本,模型的回答为:
现在我们查看同一图像的真实标签。
返回结果为:
因此,模型遵循了指令格式,仅输出了一个类别名称,但分类本身是错误的。
这正是我们需要微调的原因。基座模型具备较强的视觉理解,但并未专门训练来区分细分的骨折类型。
接下来我们将在骨折数据集上对其进行微调,以改进这一行为。
9. 配置 LoRA 以高效微调 Ministral 3
接下来我们配置LoRA,以在不更新全部参数的情况下高效微调 Ministral 3。该配置目标为 Mistral 风格视觉-语言模型中的主要注意力与 MLP 投影层。
随后定义监督微调(SFT)的训练参数。
该配置控制完整训练过程,包括批处理方式、内存优化、学习率策略、日志记录、保存策略,以及与 Hugging Face Hub 的集成。
10. 构建用于训练的视觉-语言数据 Collator
对 Ministral 3 这类视觉-语言模型进行微调,需要自定义的数据 collator,以一致的格式将图像与对话式文本批处理在一起。
下方的 collator 负责:设置分词器与填充符号、应用聊天模板、联合分词文本与图像、强制安全的最大序列长度,以及创建标签(将填充与图像 token 屏蔽为 -100)以避免影响损失计算。
11. 使用 TRL 微调 Ministral 3
在准备好 LoRA 适配器、训练配置与数据 collator 后,我们可以将一切接入 trl 的 SFTTrainer。
该训练器负责我们视觉-语言 Ministral 3 模型的完整监督微调循环,使用格式化数据集、LoRA 配置与用于图文批次的自定义 collate 函数。
运行以下代码开始训练。
在我的运行中,微调占用约 57 GB 显存,评估步骤时略高(每隔几次迭代运行一次)。实际说明在低于 70 GB 显存的硬件上也能较为从容地完成微调。
完整训练耗时约半小时,训练损失随时间稳步下降,这表明模型确实在从数据集中学习。
训练完成后,我们将适配后的模型保存至 Hugging Face Hub。它会创建一个新仓库并推送所有必要文件,便于您在任意环境中加载并运行微调模型。
您可在此查看模型:kingabzpro/ministral-3-bone-fracture。
12. 评估微调后的 Ministral 3 模型
现在我们从 Hugging Face Hub 加载微调后的模型与处理器。其底层会下载已保存的 LoRA 适配器,加载 Ministral 3 基座检查点,并将适配器合并入基座模型,准备进行推理。
为保证对比公平,我们先复用微调前测试的同一张图像(索引 200),搭配严格的分类提示词进行推理。
我们获得了完美结果。不需要额外后处理。模型遵循提示词规则,直接产出正确类别。
接着我们再试一个数据集中的随机样本,以确认这种表现并非个例。
模型输出:
真实标签:
输出:
同样,预测正确,与标签完全一致。
通过这些检验,我们可以认为 Ministral 3 14B 已成功在骨折 X 光数据集上完成微调,能够遵循指令提示词,并在这些示例上输出准确的单类预测。
总结思考
微调 Ministral 3 需要耐心和持续调试。我确实花了两整天时间,才弄清为何模型未按预期训练、以及为何尽管模型并不算大,却仍让我 GPU 频繁 OOM。
原因很简单,却容易忽略:它不仅是语言模型,还是视觉-语言模型,意味着它包含一个图像编码器,会额外消耗显存。
图像分辨率越大,显存占用越高。减小图像尺寸或从更小的 batch 开始,会让训练更稳定。
我遇到的另一问题是默认聊天模板。对于这类任务,它过长且结构化。模型花了更多精力去拟合模板格式,而不是学习如何读取图像并给出清晰的骨折分类。
在我将提示词简化为直接的指令格式后,模型终于开始正确响应视觉输入,而不是将每个答案都包装成完整的对话回复。
我还曾以为模型具备强大视觉能力,就会自然在医学影像上表现出色。这个假设拖慢了我。把它在脑子里称作“MedGemma”对模型和流程都不公平。它并非开箱即用的专科医疗模型,指望它不经适当训练就能有那样的表现,是我的错误。
最终,我克服了每个难点,并将所学整理为本指南。目的在于帮助您避开同样的困惑,更快获得有意义的结果。
“在更小的图像、更简洁的提示词与理性预期的配合下,Ministral 3 可更顺畅、更可靠地完成针对 X 光理解的微调。”
如果您想获得更多微调的实操练习,推荐Fine-Tuning with Llama 3 课程。