メインコンテンツへスキップ

グラデイントチェックポイント:Unsloth と NVIDIA で効率的にファインチューニング

Unsloth の NVIDIA 最適化グラデイントチェックポイントと QLoRA により、医療文書の OCR 向けに Qwen3.5 4B をファインチューニングする際の VRAM 使用量を削減する方法を学びます。
更新 2026年8月14日  · 12 分 読む

AIで探索

ChatGPTClaudePerplexity

Unsloth の NVIDIA との協業は、隠れたトレーニングのオーバーヘッドを削減することで、ファインチューニングを高速化することに焦点を当てています。より大きな GPU やより小さなモデルに頼るのではなく、繰り返し発生するメタデータ構築、アクティベーション再読み込みの遅延、非効率なトークンルーティングといった、学習プロセス内部のボトルネックを狙って改善します。ユーザーにとっては、対応する NVIDIA GPU 上で学習が速くなり、ファインチューニングのワークフローがよりスムーズになるという意味があります。

このガイドでは、こうした新しい Unsloth のパフォーマンス向上の内容を理解し、実用的なビジョン・ランゲージ課題にUnsloth の最適化されたファインチューニングワークフローを適用します。ここでは Qwen3.5 Vision 4B を医療用 OCR に向けてファインチューニングし、OCR データセットの医療文書風サブセットを用いて、文書画像から構造化テキストを抽出するようモデルに学習させます。

使用するものは次のとおりです。

  • Qwen3.5 4B(Vision) をベースモデルとして使用
  • 4-bit QLoRA で VRAM 使用量を削減
  • LoRA アダプタで効率的にファインチューニング
  • Unsloth のグラデイントチェックポイントで学習中のメモリ使用量を節約
  • 医療 OCR データセットの 300 サンプルのサブセット
  • ビジョン学習をスムーズにする固定サイズの画像前処理
  • 前後比較による、ベースモデルとファインチューニング後モデルの出力評価

Unsloth の NVIDIA 最適化ファインチューニングワークフローを使う

ファインチューニングを始める前に、Unsloth と NVIDIA の協業が何を改善し、このガイドとどう関係するかを理解しておくと役立ちます。

Unsloth は、NVIDIA との協業によって LLM 学習が約 25% 高速化し、精度低下はないと報告しています。これは既存の 2〜5 倍のファインチューニング高速化に上乗せされるものです。これらの利得は、モデルの学習目的を変えるのではなく、主な学習プロセスの周辺にある隠れたオーバーヘッドを削減することで得られます。言い換えると、精度を維持したまま、ファインチューニングをより速く、より効率的にすることが目標です。

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

出典: How to Make LLM Training Faster with Unsloth and NVIDIA 

学習パフォーマンスの向上

この協業では複数の性能改善が報告されています。例えば次のとおりです。

  • パックドシーケンスのメタデータキャッシュにより、Qwen3-14B の QLoRA SFT ベンチマークで 1 バッチあたり 14.3% 高速化
  • ダブルバッファの非同期グラデイントチェックポイントにより、8B モデルで 8.4%、14B モデルで 6.7%、32B モデルで 4.6% の高速化
  • GPT-OSS の MoE 学習でおよそ 10〜15% の高速化。対象ルーティング経路において順伝播が 23% 高速化、逆伝播が 13% 高速化

Unsloth と NVIDIA の協業による最大の性能向上は、パックドなテキストのみの学習や Mixture-of-Experts モデルに適用されるものが多いです。本ガイドでは Qwen3.5 Vision の OCR ファインチューニングに焦点を当てているため、それらは使用しません。

本ガイドでは NVIDIA RTX 3090 GPU を使用するため、ワークフローは NVIDIA GPU アクセラレーションと Unsloth の最適化ファインチューニング経路に基づいています。Unsloth を別のトレーナーとベンチマーク比較しているわけではないため、ここでの記述を報告された高速化の独立した証拠として読むべきではありません。代わりに、Unsloth の最適化ファインチューニングワークフローを実際のビジョン・ランゲージ課題に適用します。

グラデイントチェックポイント

このワークフローで最も関連する最適化は、Unsloth のグラデイントチェックポイントです。これは、すべてのアクティベーションを GPU メモリに保持する必要を避けることで、学習中のメモリ使用量を削減します。画像入力とテキスト出力の両方を処理する必要があるビジョン・ランゲージのファインチューニングでは特に有用です。

1. 高速ファインチューニングに向けた Unsloth のセットアップ

このガイドを実行するには、NVIDIA GPU が必要です。RunPod、Vast.ai などのプラットフォーム、または他のクラウド GPU プロバイダからレンタルできます。最初は RunPod を試しましたが、当時は利用可能な RTX 3090 の選択肢が限られていました。そのため、このワークフローでは Vast.ai の RTX 3090 GPU マシン を使用しました。

各プラットフォームの比較については、best GPU cloud providers のガイドを参照してください。

Vast.ai RTX 3090 instance

出典:  Vast.ai | Console 

インスタンスを起動したら、Jupyter Notebook を開いて新しいノートブックを作成しました。Vast.ai では、システムレベルの依存関係に影響を与えずにノートブック環境内で必要な Python パッケージをインストールできるよう、利用可能なメイン環境カーネルを選択しました。

必要パッケージのインストール

まず、Unsloth、PyTorch、ビジョンモデルの学習、データセットの読み込み、Hugging Face 連携に必要なパッケージをインストールします。

!pip install --upgrade \
    "torch>=2.8.0" "triton>=3.4.0" \
    numpy pillow torchvision bitsandbytes \
    unsloth "unsloth_zoo>=2026.4.6" \
    "datasets>=4.0.0" huggingface_hub hf_transfer pandas \
    transformers==5.2.0 torchcodec timm

これらのパッケージは公式の Unsloth ノートブック構成に基づいており、Qwen3.5 Vision の読み込み、画像・テキストデータの準備、Unsloth によるファインチューニングに必要な主要ライブラリを含みます。

CUDA デバイスの設定

次に CUDA デバイスを設定し、正しい NVIDIA GPU が利用可能かを確認します。本ガイドは RTX 3090 を使用するため、CUDA が有効か、選択された GPU の確認、CUDA と PyTorch のバージョンの表示、そして本実験に十分な VRAM があるかを検証します。

import os
import platform

CUDA_DEVICE_INDEX = 0
TARGET_GPU_NAME = "3090"

# Must be set before CUDA / Unsloth are initialized. Restart the kernel if you change these.
os.environ["CUDA_VISIBLE_DEVICES"] = str(CUDA_DEVICE_INDEX)

# RunPod + Qwen3.5 Vision OCR can hit Torch Dynamo fullgraph recompile limits.
# This disables Unsloth's torch.compile path while keeping Unsloth model loading,
# LoRA, gradient checkpointing, collator, and 8-bit optimizer benefits.
os.environ["UNSLOTH_COMPILE_DISABLE"] = "1"
os.environ["TORCH_COMPILE_DISABLE"] = "1"

import torch

DEVICE = torch.device("cuda:0")

print("Python:", platform.python_version())
print("PyTorch:", torch.__version__)
print("CUDA available:", torch.cuda.is_available())

if not torch.cuda.is_available():
    raise RuntimeError("CUDA is not available. Select a GPU instance before continuing.")

torch.cuda.set_device(0)
props = torch.cuda.get_device_properties(0)
gpu_name = torch.cuda.get_device_name(0)
total_gpu_memory_gb = props.total_memory / 1024**3

print("Selected device:", DEVICE)
print("GPU:", gpu_name)
print("CUDA version:", torch.version.cuda)
print("BF16 supported:", torch.cuda.is_bf16_supported())
print("Total GPU memory:", round(total_gpu_memory_gb, 2), "GB")

if TARGET_GPU_NAME not in gpu_name:
    raise RuntimeError(f"Expected an RTX {TARGET_GPU_NAME}, but CUDA device 0 is: {gpu_name}")

if total_gpu_memory_gb < 20:
    raise RuntimeError(f"Expected a 24 GB class 3090, but only found {total_gpu_memory_gb:.2f} GB VRAM.")

この環境では、次の GPU 構成が返されました。

Python: 3.12.13
PyTorch: 2.12.0+cu130
CUDA available: True
Selected device: cuda:0
GPU: NVIDIA GeForce RTX 3090
CUDA version: 13.0
BF16 supported: True
Total GPU memory: 23.56 GB

これにより、ノートブックが十分な VRAM を備えた NVIDIA GeForce RTX 3090 上で動作していることが確認できました。

学習設定とプロンプトの定義

GPU の確認後、モデル、データセット、学習設定、出力ディレクトリ、画像サイズ、OCR 用のプロンプトを定義します。

MODEL_NAME = "unsloth/Qwen3.5-4B"
DATASET_NAME = "naazimsnh02/medocr-vision-dataset"

SAMPLE_COUNT = 300
EVAL_INDEX = 0
MAX_LENGTH = 4096
MAX_STEPS = 30

PER_DEVICE_BATCH_SIZE = 4
GRADIENT_ACCUMULATION_STEPS = 2
LEARNING_RATE = 2e-4
SEED = 3407

OUTPUT_DIR = "outputs/qwen35_vision_medical_ocr"
ADAPTER_DIR = "qwen35-vision-medical-ocr-lora"

# Medical document images vary heavily in size. Fixed-size canvases avoid
# repeated Torch Dynamo recompiles during vision training.
# 768x1024 is a practical portrait-page compromise for a 24 GB 3090 smoke test.
FIXED_IMAGE_SIZE = (768, 1024)

# Official Unsloth Qwen3.5 Vision notebook uses False here for 16-bit LoRA.
# Set True only if you hit VRAM limits.
LOAD_IN_4BIT = True

SYSTEM_PROMPT = "You are a medical OCR transcription engine. Return only the exact text visible in the medical document image."
INSTRUCTION = "Extract all readable text from this medical document exactly. Preserve structure when possible. Return only the OCR text, with no explanation, no diagnosis, no medical advice, and no reasoning."

ここでは、Unsloth 提供の Qwen3.5 Vision 4B と医療 OCR のビジョンデータセットを使用します。本ガイドでは 300 サンプルを選び、30 ステップ学習します。これによりランが軽量でありながら、モデルが対象の OCR 形式に適応していく様子を確認できます。

768×1024 の固定画像サイズは、学習中の画像入力を一貫させるのに役立ちます。医療文書は解像度やアスペクト比のばらつきが大きいため、固定キャンバスにリサイズすることでワークフローが安定し、ビジョン・ランゲージのファインチューニング時の形状関連の問題を軽減します。

2. モデルの読み込み

環境の準備ができたら、Unsloth の FastVisionModel を使って Qwen3.5 Vision 4B モデルを読み込みます。

import unsloth
from unsloth import FastVisionModel
torch.cuda.set_device(0)

model, tokenizer = FastVisionModel.from_pretrained(
    MODEL_NAME,
    load_in_4bit=LOAD_IN_4BIT,
    use_gradient_checkpointing="unsloth",
)

print("Loaded:", MODEL_NAME)
print("4-bit:", LOAD_IN_4BIT)
print("Model device:", next(model.parameters()).device)

モデルの読み込み後、正しいモデルが読み込まれたこと、4-bit モードが有効なこと、モデルが GPU 上に配置されたことが出力で確認できます。

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

ここで、FastVisionModel.from_pretrained() はビジョン・ランゲージモデルを読み込み、より高速かつ省メモリでファインチューニングできるよう Unsloth の最適化を適用します。さらに load_in_4bit を有効にして 4-bit 精度でモデルを読み込み、VRAM 使用量を削減します。RTX 3090 のような 24 GB 級 GPU を使う際に有用です。

また、use_gradient_checkpointing="unsloth" で Unsloth のグラデイントチェックポイントも有効化しています。

これは学習中のメモリ使用量を抑えるのに役立ち、特に画像とテキストの両方を処理するビジョン・ランゲージモデルでは重要です。

3. LoRA アダプタの追加

次に LoRA アダプタ をモデルに追加します。LoRA を使うと、フルモデルを更新するのではなく、訓練可能パラメータの一部のみを微調整できます。これにより、学習が速く、省メモリで、単一 GPU でも実行しやすくなります。

 
model = FastVisionModel.get_peft_model(
    model,
    finetune_vision_layers=True,
    finetune_language_layers=True,
    finetune_attention_modules=True,
    finetune_mlp_modules=True,
    r=16,
    lora_alpha=16,
    lora_dropout=0,
    bias="none",
    random_state=SEED,
    use_rslora=False,
    loftq_config=None,
)

本ガイドでは、アダプタをモデルのビジョン部分と言語部分の両方に追加します。これにより、医療文書画像を読み取り、想定される構造化 OCR テキストを生成する方法をモデルが学習しやすくなります。このステップ後、モデルは医療 OCR データセットでの学習が可能な状態になります。

4. 医療 OCR データセットの読み込み

次に、Hugging Face から 医療 OCR データセットを読み込み、ファインチューニング用に小さなサブセットを準備します。

from datasets import load_dataset
from PIL import Image

raw_dataset = load_dataset(DATASET_NAME, split="train")

MEDICAL_KEYWORDS = [
    "doctor", "dr.", "clinic", "hospital", "patient", "medication",
    "medications", "prescription", "signature", "department", "report",
    "diagnosis", "lab", "laboratory", "blood", "hemoglobin", "mg", "dose",
    "<s_ocr>",
]

このデータセットには、文書画像とそれに対応する OCR テキストが含まれます。本ガイドでは医療風の OCR 例だけを使いたいため、単純なキーワードベースの方法でデータセットをフィルタします。doctor、clinic、patient、medication、prescription、diagnosis、投与量関連の語など、医療文書で一般的に見られる用語を検索します。

def looks_medical(sample):
    text = str(sample.get("text", "")).lower()
    return any(keyword in text for keyword in MEDICAL_KEYWORDS)

medical_indices = []
for idx, sample in enumerate(raw_dataset):
    if looks_medical(sample):
        medical_indices.append(idx)
        if len(medical_indices) >= SAMPLE_COUNT:
            break

if not medical_indices:
    raise RuntimeError("No medical-looking OCR samples found. Broaden MEDICAL_KEYWORDS or inspect the dataset text field.")

print(f"Selected {len(medical_indices)} medical-looking samples.")

これにより、医療 OCR 課題に関係がありそうな例を軽量に選択できます。本ランでは医療風サンプルを 300 件選びます。

次に、各画像を 768×1024 の固定キャンバスに正規化します。医療文書画像はサイズやアスペクト比がさまざまなため、このステップにより学習データをより一貫させます。元のアスペクト比を保ったままリサイズし、白背景の上に配置します。

def normalize_ocr_image(image, size=FIXED_IMAGE_SIZE):
    image = image.convert("RGB")
    target_w, target_h = size
    scale = min(target_w / image.width, target_h / image.height)
    new_w = max(1, int(image.width * scale))
    new_h = max(1, int(image.height * scale))
    resized = image.resize((new_w, new_h), Image.Resampling.LANCZOS)

    canvas = Image.new("RGB", size, "white")
    left = (target_w - new_w) // 2
    top = (target_h - new_h) // 2
    canvas.paste(resized, (left, top))
    return canvas

datasets.map の代わりに、シンプルな Python リストを手動で構築します。これは、PIL 画像の書き換え時に一部のクラウドノートブック環境で発生しうるハングを避けるためです。

dataset = []
for idx in medical_indices:
    sample = raw_dataset[idx]
    dataset.append(
        {
            "image": normalize_ocr_image(sample["image"]),
            "text": sample["text"],
        }
    )

print("Examples:", len(dataset))
print("Columns:", list(dataset[0].keys()))
print("Fixed image size:", dataset[EVAL_INDEX]["image"].size)
print("Sample text:", dataset[EVAL_INDEX]["text"])

前処理後、各サンプルは正規化済み画像と目標 OCR テキストの 2 フィールドを持ちます。

Examples: 300
Columns: ['image', 'text']
Fixed image size: (768, 1024)
Sample text: <s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

リサイズされた例のプレビューも確認できます。

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

プレビューには、クリニック情報、医師名、患者情報、投薬、署名が記載された医療文書風の画像が表示されます。これにより、このデータセットが OCR ファインチューニングに適していることが確認できます。

Doctors prescription.

5. サンプルをビジョン用会話形式に変換

データセットが読み込まれ、画像が正規化できたら、各サンプルを Qwen3.5 Vision が期待する会話形式に変換する必要があります。

各学習サンプルは次の 3 要素を含みます。

  • モデルの役割を医療 OCR の転記エンジンとして定義するシステムメッセージ
  • 画像と OCR 指示を含むユーザーメッセージ
  • 期待される OCR 出力を含むアシスタントメッセージ
def build_ocr_messages(image=None, target_text=None, instruction=INSTRUCTION):
    user_content = [
        {"type": "image"},
        {"type": "text", "text": instruction},
    ]

    if image is not None:
        user_content[0]["image"] = image

    messages = [
        {"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
        {"role": "user", "content": user_content},
    ]

    if target_text is not None:
        messages.append(
            {
                "role": "assistant",
                "content": [{"type": "text", "text": target_text}],
            }
        )

    return messages

上のヘルパー関数は、学習時と推論時の両方で使用するメッセージ構造を作成します。学習時は、アシスタントの応答として目標 OCR テキストを含めます。推論時は、画像と指示のみを与え、モデルに OCR テキストの生成を求めます。

続いて、データセット内のすべてのサンプルをこの会話形式に変換します。

def convert_to_conversation(sample):
    return {
        "messages": build_ocr_messages(
            image=sample["image"],
            target_text=sample["text"],
        )
    }


converted_dataset = [convert_to_conversation(sample) for sample in dataset]
converted_dataset[0]

変換後、各サンプルはメッセージのリストを含みます。最初の例には、システムプロンプト、医療文書画像、OCR 指示、期待される構造化 OCR 転記が含まれます。この形式により、画像と指示を正しいテキスト出力にマッピングする方法をモデルが学習できます。

{'messages': [{'role': 'system',
   'content': [{'type': 'text',
     'text': 'You are a medical OCR transcription engine. Return only the exact text visible in the medical document image.'}]},
  {'role': 'user',
   'content': [{'type': 'image',
     'image': <PIL.Image.Image image mode=RGB size=768x1024>},
    {'type': 'text',
     'text': 'Extract all readable text from this medical document exactly. Preserve structure when possible. Return only the OCR text, with no explanation, no diagnosis, no medical advice, and no reasoning.'}]},
  {'role': 'assistant',
   'content': [{'type': 'text',
     'text': '<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>'}]}]}

6. ファインチューニング前のベースモデル評価

学習の前に、OCR の例でベースモデルをテストしておきます。これにより、ファインチューニング前後の出力を比較するための基準点が得られます。

まず、モデルのチャットテンプレートを適用するヘルパー関数を定義します。トークナイザーのバージョンによっては enable_thinking=False がサポートされない場合があるため、フォールバックを用意して互換性を保ちます。

def render_ocr_chat_template(tokenizer, messages):
    try:
        return tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
            enable_thinking=False,
        )
    except TypeError:
        return tokenizer.apply_chat_template(
            messages,
            tokenize=False,
            add_generation_prompt=True,
        )

次に生成関数を定義します。OCR プロンプトを構築し、画像とテキストの指示を両方ともトークナイザーに渡し、モデル出力を生成して、新たに生成されたトークンのみをデコードします。

def generate_ocr_text(model, tokenizer, image, instruction=INSTRUCTION, max_new_tokens=512):
    messages = build_ocr_messages(instruction=instruction)
    input_text = render_ocr_chat_template(tokenizer, messages)
    inputs = tokenizer(
        images=image,
        text=input_text,
        add_special_tokens=False,
        return_tensors="pt",
    ).to(DEVICE)

    with torch.inference_mode():
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            use_cache=True,
            do_sample=False,
            temperature=None,
            top_p=None,
        )

    prompt_length = inputs["input_ids"].shape[-1]
    generated_tokens = outputs[:, prompt_length:]
    return tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0]

モデルを推論モードに切り替え、最初の評価画像に対して OCR テキストを生成します。

FastVisionModel.for_inference(model)
eval_image = dataset[EVAL_INDEX]["image"]
base_output = generate_ocr_text(model, tokenizer, eval_image)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nBase model output:")
print(base_output)

ベースモデルの出力は可読ではあるものの、目標の構造に厳密には従っていません。

Target:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Base model output:
Meadowview Health
45 Oak Ave.
Prescribed by: Dr. A. Smith
Date: 2024-12-16
Patient: John Doe, Age: 35
Hydrochlorothiazide 25 mg - Before meals
Signature: Dr. A. Smith

これは有用な出発点です。ベースモデルはすでに文書の多くを読み取れますが、データセットで用いられている構造化形式ではなく、自然な OCR 風の出力になっています。ファインチューニングにより、ターゲット形式への整合が進み、応答がより一貫するはずです。

7. モデルの学習

データセットが正しいビジョン会話形式になったので、TRL の SFTTrainer と Unsloth のビジョン用データコラトラを用いてモデルを学習します。

from unsloth.trainer import UnslothVisionDataCollator
from trl import SFTTrainer, SFTConfig

FastVisionModel.for_training(model)

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    data_collator=UnslothVisionDataCollator(model, tokenizer),
    train_dataset=converted_dataset,
    args=SFTConfig(
        per_device_train_batch_size=PER_DEVICE_BATCH_SIZE,
        gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
        warmup_steps=5,
        max_steps=MAX_STEPS,
        learning_rate=LEARNING_RATE,
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.001,
        lr_scheduler_type="linear",
        seed=SEED,
        output_dir=OUTPUT_DIR,
        report_to="none",
        remove_unused_columns=False,
        dataset_text_field="",
        dataset_kwargs={"skip_prepare_dataset": True},
        max_length=MAX_LENGTH,
    ),
)
trainer_stats = trainer.train()

まず FastVisionModel.for_training(model) でモデルを学習モードに切り替えます。その後、変換済みの OCR データセットを使ってトレーナーを作成します。

ここで重要なのは UnslothVisionDataCollator です。これはビジョン・ランゲージ課題であるため、学習器は医療文書画像と目標 OCR テキストの両方を正しく扱う必要があります。コラトラは、教師ありファインチューニングのためにこれらのマルチモーダル例をモデルへ渡せる形に整えます。

本ガイドでは 30 ステップ学習し、デバイスあたりバッチサイズ 4、勾配蓄積 2(実効バッチサイズ 8)で実行します。これによりランを軽量に保ちながら、モデルが構造化 OCR 形式に適応し始める様子を示せます。

Fine-tuning the Qwen 3.5 4b Vision language model

学習中、Unsloth は、サンプル数、ステップ数、バッチ数、学習可能パラメータ数、メモリ節約機能など、セットアップに関する有用な情報を出力します。このランでは、逆伝播のダブルバッファリングが有効であると報告され、グラデイントチェックポイント中の待ち時間削減に寄与しています。

8. ファインチューニング後モデルの評価

学習後、モデルを推論モードに戻し、ファインチューニング前と同じ評価画像に対して OCR テキストを生成します。

FastVisionModel.for_inference(model)
fine_tuned_output = generate_ocr_text(model, tokenizer, eval_image)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nBase model output:")
print(base_output)
print("\nFine-tuned output:")
print(fine_tuned_output)

ファインチューニング後、モデルの出力はデータセットの目標構造にかなり近づきます。

Target:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

Base model output:
Meadowview Health
45 Oak Ave.
Prescribed by: Dr. A. Smith
Date: 2024-12-16
Patient: John Doe, Age: 35
Hydrochlorothiazide 25 mg - Before meals
Signature: Dr. A. Smith


Fine-tuned output:
<s_ocr> doctor_name: Dr. A. Smith clinic_name: Meadowview Health clinic_address: 45 Oak Ave. patient_name: John Doe patient_age: 35 date: 2024-12-16 medications: - Hydrochlorothiazide 25 mg - Before meals signature: Dr. A. Smith </s>

これにより、ファインチューニング済みモデルが期待される OCR 応答形式を学習できたことが分かります。ベースモデルも可視テキストの大半を抽出できていましたが、ファインチューニング後は学習データで用いられる構造化形式により一貫して従うようになりました。

別のデータセット例でもモデルをテストできます。

EVAL_INDEX_2 = 35
eval_image_2 = dataset[EVAL_INDEX_2]["image"]

fine_tuned_output = generate_ocr_text(model, tokenizer, eval_image_2)

print("Target:")
print(dataset[EVAL_INDEX]["text"])
print("\nFine-tuned output:")
print(fine_tuned_output)

2 つ目の例では、期待される構造に従っているものの、Amlodipine を Amoxicillin と誤って生成する軽微な OCR ミスがあります。

Target:
<s_ocr> doctor_name: Dr. C. Rossi clinic_name: Riverside Clinic clinic_address: 45 Oak Ave. patient_name: Wei Li patient_age: 70 date: 2024-12-16 medications: - Acetaminophen 20 mg - Take twice daily - Amlodipine 20 mg - After meals signature: Dr. C. Rossi </s>

Fine-tuned output:
<s_ocr> doctor_name: Dr. C. Rossi clinic_name: Riverside Clinic clinic_address: 45 Oak Ave. patient_name: Wei Li patient_age: 70 date: 2024-12-16 medications: - Acetaminophen 20 mg - Take twice daily - Amoxicillin 20 mg - After meals signature: Dr. C. Rossi </s>

これは、形式の整合は改善されている一方で、OCR の正確性はデータ品質、画像の鮮明さ、学習規模、ファインチューニングステップ数に依存することを思い出させます。実運用の OCR システムでは、より大規模で多様なデータセットで学習し、幅広い例で精度を評価する必要があります。

9. ファインチューニング済みアダプタの保存

学習が完了したら、LoRA アダプタとトークナイザーをローカルに保存します。

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

print("Saved adapter to:", ADAPTER_DIR)

出力で、アダプタが保存されたことが確認できます。

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

ここで保存されるのはファインチューニング済みのアダプタ重みのみで、ベースモデルの完全なコピーではありません。後でベースの Qwen3.5-4B モデルを再読み込みし、このアダプタを適用すれば、ファインチューニングした OCR の挙動を再利用できます。これにより保存物は軽量になり、保管・共有・デプロイが容易になります。

まとめ

学習プロセスは単一の NVIDIA RTX 3090 でも軽量かつ実用的でした。ビジョン・ランゲージのファインチューニングは通常メモリ負荷が高いにもかかわらず、想定より大幅に少ない VRAM で済みました。最大でも約 14 GB、平均では 9 GB 近くで推移し、Qwen3.5 Vision のファインチューニングとしては印象的です。

モデルの適応も速く、わずかなステップで出力が目標の OCR 形式にかなり近づきました。ベースモデルでも文書を読み取れていましたが、ファインチューニング後はデータセットの形式により一貫して従うようになりました。

とはいえ、セットアップ体験が完璧というわけではありません。Unsloth のインストールには多くの試行錯誤を要しました。ローカル環境や仮想環境、CUDA バージョン、クラウド GPU プロバイダが混在する状況では、正しく構成するのが難しいことがあります。

場合によっては CUDA の互換性問題で環境が壊れ、デバッグに想定以上の時間がかかることもあります。クラウド GPU プラットフォームで Unsloth の Docker イメージから始めても、環境がそのままクリーンに動作しないと、時間を要する可能性があります。

もう一つの重要な教訓は、モデルのテンプレートが重要だということです。データセットが正しいチャット/ビジョン会話形式に変換されていないと、モデルはうまく学習できません。Qwen3.5 Vision では、正しい画像・テキストのメッセージ構造を使うことが不可欠です。適切なテンプレートがなければ、学習自体は走っても、モデルは実際には課題に適応しない可能性があります。

総じて、Unsloth は限られた GPU リソースでも、ローカルマシンやレンタル GPU 上で効率よくモデルをファインチューニングしたいユーザーにとって有力な選択肢です。メモリ使用量を減らし、小規模ハードウェアの有用性を高め、実験のスピードアップにもつながります。ただし、日常的にファインチューニングや学習を行うユーザーにとっては、セットアップの複雑さが負担になる場合があります。標準的な Transformers ベースの学習は、より安定していて、インストールしやすく、環境間で再現もしやすいことが多いです。

インストールの煩雑さがネックであれば、手動の環境セットアップなしに Unsloth のローカル Web UI で Qwen3.5-9B をファインチューニングする方法を解説したUnsloth Studio のガイドを参照することをおすすめします。

トピック

おすすめの AI コース

Courses

PyTorchで学ぶIntroduction to Deep Learning

4時間
88.5K
PyTorchで最初のニューラルネットワークを構築し、ハイパーパラメータを調整して、分類と回帰の問題に取り組む方法を学びます。
詳細を見るRight Arrow
コースを開始

Courses

Intermediate Deep Learning with PyTorch

4時間
28.4K
画像データや時系列データのモデリングに用いられる、CNN(畳み込みニューラルネットワーク)、RNN(再帰型ニューラルネットワーク)、LSTM(長短期記憶ネットワーク)、GRU(ゲート再帰ユニット)といった基本的な深層学習アーキテクチャについて学びましょう。
もっと見るRight Arrow