Llama 3.2 Vision Finetuning Unsloth Radiography （📒Googgle colabノートブック付）

## はじめに

このノートブックでは、医療画像分析のための大規模言語ビジョンモデル(Vision Language Model: VLM)のファインチューニングを行います。具体的には、Llama 3.2 11Bモデルを使用して、X線画像、CTスキャン、超音波画像を分析し、医療専門家をサポートするためのモデル調整を学びます。

主な特徴:
- Unslothライブラリを使用した効率的なトレーニング
- 医療画像データセットを使用した実践的なファインチューニング
- 最小限のGPUメモリ使用で11Bパラメータモデルを扱う方法
- LoRAによる効率的なパラメータ調整

**無料のTesla T4 Google Colabインスタンスで実行できます！**

<div class="align-center">
  <a href="https://github.com/unslothai/unsloth"><img src="https://github.com/unslothai/unsloth/raw/main/images/unsloth%20new%20logo.png" width="115"></a>
  <a href="https://discord.gg/unsloth"><img src="https://github.com/unslothai/unsloth/raw/main/images/Discord%20button.png" width="145"></a>
  <a href="https://docs.unsloth.ai/"><img src="https://github.com/unslothai/unsloth/blob/main/images/documentation%20green%20button.png?raw=true" width="125"></a>
  サポートが必要な場合はDiscordに参加してください + ⭐ <i><a href="https://github.com/unslothai/unsloth">Github</a>でスターをください</i> ⭐
</div>

自身のコンピュータにUnslothをインストールする場合は、Githubページの[インストール手順](https://github.com/unslothai/unsloth?tab=readme-ov-file#-installation-instructions)に従ってください。

**[NEW] 2024年11月現在、Unslothはビジョンファインチューニングをサポートしています！**

このノートブックでは以下を学びます:
- [データ準備](#Data)
- [モデルの訓練](#Train)
- [モデルの実行](#Inference)
- [モデルの保存](#Save)

```python
%%capture
!pip install unsloth
# 最新のnightly Unslothバージョンを取得
!pip uninstall unsloth -y && pip install --upgrade --no-cache-dir --no-deps git+https://github.com/unslothai/unsloth.git
```

以下のモデルをサポートしています:
* Llama 3.2 Vision 11B, 90B
* Pixtral
* Qwen2VL 2B, 7B, 72B
* LlavaのすべてのバリアントやLlava NEXTなど

16ビットLoRAを `load_in_4bit=False` で、または4ビットQLoRAを使用できます。どちらも高速化され、少ないメモリ使用量で動作します。

```python
from unsloth import FastVisionModel # FastLanguageModel for LLMs
import torch

# 4bit pre quantized models we support for 4x faster downloading + no OOMs.
fourbit_models = [
    "unsloth/Llama-3.2-11B-Vision-Instruct-bnb-4bit", # Llama 3.2 vision support
    "unsloth/Llama-3.2-11B-Vision-bnb-4bit",
    "unsloth/Llama-3.2-90B-Vision-Instruct-bnb-4bit", # Can fit in a 80GB card!
    "unsloth/Llama-3.2-90B-Vision-bnb-4bit",

    "unsloth/Pixtral-12B-2409-bnb-4bit",              # Pixtral fits in 16GB!
    "unsloth/Pixtral-12B-Base-2409-bnb-4bit",         # Pixtral base model

    "unsloth/Qwen2-VL-2B-Instruct-bnb-4bit",          # Qwen2 VL support
    "unsloth/Qwen2-VL-7B-Instruct-bnb-4bit",
    "unsloth/Qwen2-VL-72B-Instruct-bnb-4bit",

    "unsloth/llava-v1.6-mistral-7b-hf-bnb-4bit",      # Any Llava variant works!
    "unsloth/llava-1.5-7b-hf-bnb-4bit",
] # More models at https://huggingface.co/unsloth

model, tokenizer = FastVisionModel.from_pretrained(
    "unsloth/Llama-3.2-11B-Vision-Instruct",
    load_in_4bit = True, # Use 4bit to reduce memory use. False for 16bit LoRA.
    use_gradient_checkpointing = "unsloth", # True or "unsloth" for long context
)
```

```python
model = FastVisionModel.get_peft_model(
    model,
    finetune_vision_layers     = True,  # ビジョン層をファインチューニングしない場合はFalse
    finetune_language_layers   = True,  # 言語層をファインチューニングしない場合はFalse 
    finetune_attention_modules = True,  # アテンション層をファインチューニングしない場合はFalse
    finetune_mlp_modules      = True,  # MLP層をファインチューニングしない場合はFalse

    r = 16,            # 大きいほど精度が高くなりますが過学習の可能性があります
    lora_alpha = 16,   # 推奨: alpha == r 以上
    lora_dropout = 0,
    bias = "none",
    random_state = 3407,
    use_rslora = False,   # ランク安定化LoRAをサポート
    loftq_config = None,  # LoftQもサポート
)
```

## データ準備 <a name="Data"></a>

ROCOラジオグラフィーデータセットのサンプル版を使用します。データセットは[こちら](https://huggingface.co/datasets/unsloth/Radiology_mini)からアクセスできます。完全なデータセットは[こちら](https://huggingface.co/datasets/eltorio/ROCOv2-radiology)です。

このデータセットには、医療条件や疾患を示すX線画像、CTスキャン、超音波画像が含まれています。各画像には、専門家が書いた説明文が付いています。目的は、VLMを医療専門家のための有用な分析ツールとなるようにファインチューニングすることです。

```python
from datasets import load_dataset
dataset = load_dataset("unsloth/Radiology_mini", split = "train")
```

データセットを確認してみましょう。最初の例を見てみましょう：

```python
dataset
```

```python
dataset[0]["image"]
```

```python
dataset[0]["caption"]
```

データセットをフォーマットするために、すべてのビジョンファインチューニングタスクは以下の形式でフォーマットする必要があります：

```python
[
{ "role": "user",
  "content": [{"type": "text",  "text": instruction}, {"type": "image", "image": image} ]
},
{ "role": "assistant",
  "content": [{"type": "text",  "text": answer} ]
},
]
```

VLMに専門の放射線技師になってもらうためのカスタム指示を作成します。1つの指示だけでなく、複数のターンを追加して動的な会話にすることもできます。

```python
instruction = "You are an expert radiographer. Describe accurately what you see in this image."

def convert_to_conversation(sample):
    conversation = [
        { "role": "user",
          "content" : [
            {"type" : "text",  "text"  : instruction},
            {"type" : "image", "image" : sample["image"]} ]
        },
        { "role" : "assistant",
          "content" : [
            {"type" : "text",  "text"  : sample["caption"]} ]
        },
    ]
    return { "messages" : conversation }
pass
```

データセットをファインチューニング用の「正しい」形式に変換しましょう：

```python
converted_dataset = [convert_to_conversation(sample) for sample in dataset]
```

最初の例は以下のような構造になっています：

```python
converted_dataset[0]
```

ファインチューニングを行う前に、ビジョンモデルがすでに画像を分析する方法を知っているかもしれません。確認してみましょう！

```python
FastVisionModel.for_inference(model) # 推論のために有効化！

image = dataset[0]["image"]
instruction = "You are an expert radiographer. Describe accurately what you see in this image."

messages = [
    {"role": "user", "content": [
        {"type": "image"},
        {"type": "text", "text": instruction}
    ]}
]
input_text = tokenizer.apply_chat_template(messages, add_generation_prompt = True)
inputs = tokenizer(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to("cuda")

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

## モデルの訓練 <a name="Train"></a>

ここではHugging Face TRLの`SFTTrainer`を使用します！詳しい情報は[TRL SFTのドキュメント](https://huggingface.co/docs/trl/sft_trainer)を参照してください。
処理を高速化するために60ステップで実行しますが、完全な実行を行う場合は`num_train_epochs=1`を設定し、`max_steps=None`をオフにすることができます。
TRLの`DPOTrainer`もサポートしています！

ビジョンファインチューニングのセットアップを支援する新しい`UnslothVisionDataCollator`を使用します。

```python
from unsloth import is_bf16_supported
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 = 2,
        per_device_train_batch_size = 4,
        gradient_accumulation_steps = 4,
        warmup_steps = 5,
        max_steps = 30,
        # num_train_epochs = 1, # 完全なトレーニング実行の場合はこちらを設定
        learning_rate = 2e-4,
        fp16 = not is_bf16_supported(),
        bf16 = is_bf16_supported(),
        logging_steps = 1,
        optim = "adamw_8bit",
        weight_decay = 0.01,
        lr_scheduler_type = "linear",
        seed = 3407,
        output_dir = "outputs",
        report_to = "none",     # Weights and Biases用

        # ビジョンファインチューニングには以下の項目が必須です：
        remove_unused_columns = False,
        dataset_text_field = "",
        dataset_kwargs = {"skip_prepare_dataset": True},
        dataset_num_proc = 4,
        max_seq_length = 2048,
    ),
)
```

```python
#@title 現在のメモリ統計を表示
gpu_stats = torch.cuda.get_device_properties(0)
start_gpu_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)
print(f"GPU = {gpu_stats.name}. 最大メモリ = {max_memory} GB.")
print(f"{start_gpu_memory} GB のメモリが予約済み.")
```

```python
trainer_stats = trainer.train()
```

```python
#@title 最終的なメモリと時間の統計を表示
used_memory = round(torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3)
used_memory_for_lora = round(used_memory - start_gpu_memory, 3)
used_percentage = round(used_memory         /max_memory*100, 3)
lora_percentage = round(used_memory_for_lora/max_memory*100, 3)
print(f"トレーニングに {trainer_stats.metrics['train_runtime']} 秒使用.")
print(f"トレーニングに {round(trainer_stats.metrics['train_runtime']/60, 2)} 分使用.")
print(f"ピーク予約メモリ = {used_memory} GB.")
print(f"トレーニングのためのピーク予約メモリ = {used_memory_for_lora} GB.")
print(f"最大メモリに対するピーク予約メモリの割合 = {used_percentage} %.")
print(f"最大メモリに対するトレーニング用ピーク予約メモリの割合 = {lora_percentage} %.")
```

## 推論 <a name="Inference"></a>

モデルを実行してみましょう！指示と入力を変更できます - 出力は空白のままにしておいてください！

`min_p = 0.1`と`temperature = 1.5`を使用します。これらの設定についての詳細は、この[ツイート](https://x.com/menhguin/status/1826132708508213629)を参照してください。

```python
FastVisionModel.for_inference(model) # 推論のために有効化！

image = dataset[0]["image"]
instruction = "You are an expert radiographer. Describe accurately what you see in this image."

messages = [
    {"role": "user", "content": [
        {"type": "image"},
        {"type": "text", "text": instruction}
    ]}
]
input_text = tokenizer.apply_chat_template(messages, add_generation_prompt = True)
inputs = tokenizer(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to("cuda")

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

## モデルの保存 <a name="Save"></a>

最終的なモデルをLoRAアダプターとして保存するには、オンライン保存用のHugging Faceの`push_to_hub`またはローカル保存用の`save_pretrained`を使用します。

**[注意]** これはLoRAアダプターのみを保存し、完全なモデルは保存しません。16bitまたはGGUFで保存する場合は、下にスクロールしてください！

```python
model.save_pretrained("lora_model") # ローカル保存
tokenizer.save_pretrained("lora_model")
# model.push_to_hub("your_name/lora_model", token = "...") # オンライン保存
# tokenizer.push_to_hub("your_name/lora_model", token = "...") # オンライン保存
```

今保存したLoRAアダプターを推論用に読み込む場合は、`False`を`True`に変更してください：

```python
if False:
    from unsloth import FastVisionModel
    model, tokenizer = FastVisionModel.from_pretrained(
        model_name = "lora_model", # トレーニングに使用したモデル
        load_in_4bit = load_in_4bit,
    )
    FastVisionModel.for_inference(model) # 推論のために有効化！

image = dataset[0]["image"]
instruction = "You are an expert radiographer. Describe accurately what you see in this image."

messages = [
    {"role": "user", "content": [
        {"type": "image"},
        {"type": "text", "text": instruction}
    ]}
]
input_text = tokenizer.apply_chat_template(messages, add_generation_prompt = True)
inputs = tokenizer(
    image,
    input_text,
    add_special_tokens = False,
    return_tensors = "pt",
).to("cuda")

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

## VLLMのためのfloat16での保存

`float16`での直接保存もサポートしています。float16の場合は`merged_16bit`を選択してください。Hugging Faceアカウントにアップロードするには`push_to_hub_merged`を使用してください！個人用トークンは https://huggingface.co/settings/tokens で取得できます。

```python
# 保存は1つだけ選択してください！（両方は必要ありません）

# 16ビットでローカルに保存
if False: model.save_pretrained_merged("unsloth_finetune", tokenizer,)

# Hugging Faceアカウントにエクスポートして保存
if False: model.push_to_hub_merged("YOUR_USERNAME/unsloth_finetune", tokenizer, token = "PUT_HERE")
```

以上で完了です！Unslothについて質問がある場合は、[Discord](https://discord.gg/unsloth)チャンネルがあります！バグを見つけた場合や、最新のLLM情報を追いかけたい場合、またはヘルプが必要な場合、プロジェクトに参加したい場合などは、参加してみましょう！

## 📒ノートブック

https://colab.research.google.com/drive/1F-ZnfWJs0O9-EGYPPE-PjlceIa_o6eYa?usp=sharing

## モデルのURL

https://huggingface.co/MakiAi/Llama3.2-11B-Radiography-v0

https://huggingface.co/MakiAi/Llama3.2-11B-Radiography-v0-float16


## その他の有用なリンク:

1. Llama 3.2 会話用ノートブック [無料Colab](https://colab.research.google.com/drive/1T5-zKWM_5OD21QHwXHiV9ixTRR7k3iB9?usp=sharing)
2. Ollamaへのファインチューニング保存 [無料ノートブック](https://colab.research.google.com/drive/1WZDi7APtQ9VsvOrQSSC5DDtxq159j8iZ?usp=sharing)
3. その他のノートブックは[Github](https://github.com/unslothai/unsloth)をご覧ください！