🦙

Llama3.2の使い方とファインチューニングの方法

2024/10/22に公開

内容

2024年9月25日にMetaから新たなLlama3.2が発表された.Llama3.2シリーズにはテキストモデルである1B・3B,画像を理解し指示に応答できる11B・90Bのビジョンモデルが含まれている.これらのモデルは自然言語処理や画像認識タスクに対応しており,高度な応答生成や画像理解の能力を備えている.

今回は,特にビジョンモデルである「Llama-3.2-11B-Vision-Instruct」について,その使い方とファインチューニングの手順を解説する.

参考

注意:Llama-3.2-11B-Vision-InstructはGPUメモリ(VRAM)が23GBほど必要なため,
実行にはGPUメモリを増やすかモデルを量子化する必要があると思われる.

Hugging Faceのアカウント作成・ログイン

Hugging Faceとは,機械学習を行う際に使用するツールやプログラムを誰でも使えるように公開・共有するためのプラットフォームである.今回はLlama3.2を使うためにHugging Faceを使う.そのために以下の手順でアカウント作成とログインを行う.

  1. [https://huggingface.co/]を開き,Sign Up を選ぶ(下記画像の①)
  2. メール,パスワード,その他必須項目を入力して先に進む
  3. メールが届くので,確認リンクを開き,画面にYour email address has been verified successfully. と表示されていればアカウント作成完了
  4. [https://huggingface.co/]に戻り,Log In を選び,ログインする(下記画像の②)
    image.png

アクセストークンの発行

APIやライブラリを使用するためにアクセストークンを発行する必要があるため,以下の手順で発行する.

  1. [https://huggingface.co/]にアクセスし,ログインする
  2. [https://huggingface.co/settings/tokens]にアクセスし,Create new token をクリックする
    image.png
  3. 使用目的に沿ったToke typeを選択し,任意の名前を入力し,Create token でトークンを作成する.Toke typeに関しては[https://huggingface.co/docs/hub/security-tokens]を参照.
    トークンの作成
    トークンの作成
    Token typeの説明
    Token typeの説明
  4. トークンが発行されるので,忘れないように保存しておく
    image.png
    トークンの生成
    トークンの生成

コマンドラインからHugging Faceにログイン

モデルのダウンロード等にはコマンドラインからHugging Faceにログインする必要がある.
以下の手順でログインを行う.

  1. huggingface_hubのインストール
$ pip install -U "huggingface_hub[cli]"
  1. ログインする.ここでアクセストークンを求められるので,入力する.
$ huggingface-cli login
ここでアクセストークン入力
$ huggingface-cli whoami ←ログインしているアカウント名を表示
{user name}

Hugging Faceで認証を受ける

Llama3.2の実装のためにHugging Faceで認証を受ける必要がある.
以下の手順で認証を受ける.

  1. 使用するモデルのページ[https://huggingface.co/meta-llama/Llama-3.2-11B-Vision-Instruct]にアクセスする
  2. Model cardYou need to agree to share your contact information to access this model を見つけ,Expand to review and access をクリックする
    image.png
  3. 名前など各項目を入力して,Submitをクリックする
    image.png
  4. しばらくすると認証メールが来るので,メールが確認できたら認証完了.これで準備完了.

Llama-3.2-11B-Vision-Instructデモプログラム実行

[https://huggingface.co/meta-llama/Llama-3.2-11B-Vision-Instruct]に記載されているデモプログラムを実行してみる.
このコードはウサギの画像を読み込み,俳句のような文章を生成させるコードである.

  1. 必要なライブラリ等のインストール,GPUの使用準備を行う
  2. 以下のコードを実行
import requests
import torch
from PIL import Image
from transformers import MllamaForConditionalGeneration, AutoProcessor

model_id = "meta-llama/Llama-3.2-11B-Vision-Instruct"

model = MllamaForConditionalGeneration.from_pretrained(
    model_id,
    torch_dtype=torch.bfloat16,
    device_map="auto",
)
processor = AutoProcessor.from_pretrained(model_id)

url = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/0052a70beed5bf71b92610a43a52df6d286cd5f3/diffusers/rabbit.jpg"
image = Image.open(requests.get(url, stream=True).raw)

messages = [
    {"role": "user", "content": [
        {"type": "image"},
        {"type": "text", "text": "If I had to write a haiku for this one, it would be: "}
    ]}
]
input_text = processor.apply_chat_template(messages, add_generation_prompt=True)
inputs = processor(
    image,
    input_text,
    add_special_tokens=False,
    return_tensors="pt"
).to(model.device)

output = model.generate(**inputs, max_new_tokens=30)
print(processor.decode(output[0]))

  1. 実行結果
<|image|>If I had to write a haiku for this one, it would be: <|eot_id|><|start_header_id|>assistant<|end_header_id|>

Here is a haiku for the image:

Rabbit in a coat
Standing on a dirt path
Village in the distance<|eot_id|>

ファインチューニングのためのデータセット作成

これからファインチューニングの作業を進める.まず最初に行うのは学習用データセットの作成である.
今回は,トイプードルと唐揚げの見た目が似ていることから,ファインチューニングすることで画像に写っている対象がトイプードルか唐揚げかを判定できるモデル作成を目指す.学習用データセットには,トイプードルと唐揚げの画像をそれぞれ15枚ずつ収集し,JSONファイルを用いてそれぞれに正しいラベルを付けることで,モデルの学習に必要な情報を提供する.
まずは以下に,15枚ずつ収集した唐揚げ画像が保存されたフォルダkaraage/kara*.jpegと,トイプードル画像が保存されたフォルダtoypoodle/toy*.jpegの構造を示す.

├── karaage/
│   ├── kara1.jpeg
│   ├── kara2.jpeg
│   ├── kara3.jpeg
│   ├── kara4.jpeg
│   ├── kara5.jpeg
│   ├── kara6.jpeg
│   ├── kara7.jpeg
│   ├── kara8.jpeg
│   ├── kara9.jpeg
│   ├── kara10.jpeg
│   ├── kara11.jpeg
│   ├── kara12.jpeg
│   ├── kara13.jpeg
│   ├── kara14.jpeg
│   └── kara15.jpeg
└── toypoodle/
    ├── toy1.jpeg
    ├── toy2.jpeg
    ├── toy3.jpeg
    ├── toy4.jpeg
    ├── toy5.jpeg
    ├── toy6.jpeg
    ├── toy7.jpeg
    ├── toy8.jpeg
    ├── toy9.jpeg
    ├── toy10.jpeg
    ├── toy11.jpeg
    ├── toy12.jpeg
    ├── toy13.jpeg
    ├── toy14.jpeg
    └── toy15.jpeg

次に,画像ファイル名file_name,その画像に関する質問question,およびその質問に対する回答answerを組み合わせたJSONファイル(annotations.json)を作成する.このannotations.jsonは,モデルが画像の内容を理解し,トイプードルと唐揚げを正しく分類できるようにするための学習データとして使用する.

[
    {
        "file_name": "karaage/kara1.jpeg",
        "question": "この画像には唐揚げかトイプードルのどちらかが写っています。どちらか答えてください。",
        "answer": "この画像には唐揚げが写っています。"
    },
    {
        "file_name": "karaage/kara2.jpeg",
        "question": "この画像には唐揚げかトイプードルのどちらかが写っています。どちらか答えてください。",
        "answer": "この画像には唐揚げが写っています。"
    },
            .
            .
            .
    {
        "file_name": "karaage/kara15.jpeg",
        "question": "この画像には唐揚げかトイプードルのどちらかが写っています。どちらか答えてください。",
        "answer": "この画像には唐揚げが写っています。"
    },
    {
        "file_name": "toypoodle/toy1.jpeg",
        "question": "この画像には唐揚げかトイプードルのどちらかが写っています。どちらか答えてください。",
        "answer": "この画像にはトイプードルが写っています。"
    },
    {
        "file_name": "toypoodle/toy2.jpeg",
        "question": "この画像には唐揚げかトイプードルのどちらかが写っています。どちらか答えてください。",
        "answer": "この画像にはトイプードルが写っています。"
    },
            .
    	    .
            .
    },
    {
        "file_name": "toypoodle/toy15.jpeg",
        "question": "この画像には唐揚げかトイプードルのどちらかが写っています。どちらか答えてください。",
        "answer": "この画像にはトイプードルが写っています。"
    }
]

次に以下のCreateDataset.pyを用いて,作成したannotations.jsonからデータセットprocessed_datasetを作成する.

from datasets import load_dataset, Dataset
from PIL import Image as PILImage
import json
import io

# JSONファイルを読み込む
annotations_path = "annotations.json"
with open(annotations_path, 'r') as f:
    annotations = json.load(f)

# JSONデータを使ってデータセットを作成
dataset = load_dataset('json', data_files=annotations_path)

# 'file_name'列を使って画像データをロードする関数を定義
def load_image(example):
    image_path = f"./trainImage/{example['file_name']}"
    with open(image_path, "rb") as image_file:
        image = PILImage.open(io.BytesIO(image_file.read()))
        image = image.convert("RGB")  # 画像をRGB形式に変換(必要に応じて)
        example['image'] = image
    return example

# 画像データをデータセットに追加
dataset = dataset.map(load_image)

# 'file_name'列を削除する関数を定義
def remove_file_name(example):
    example.pop('file_name', None)  # 'file_name'キーを削除
    return example

# 'file_name'列をデータセットから削除
dataset = dataset.map(remove_file_name)

# データセットを保存
dataset.save_to_disk("./processed_dataset")

ファインチューニング

次に以下のLlmTrain.pyを用いて,作成したprocessed_datasetを使用し,LoRA(Low-Rank Adaptation)という手法を用いてファインチューニングを行い,訓練モデル(./lora ディレクトリ内)を作成する.

# -------データセットの読み込み----------
from datasets import load_from_disk

ds = load_from_disk("processed_dataset/train")
print(ds)

# -------ファインチューニング----------
from transformers import MllamaForConditionalGeneration, AutoProcessor, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
import torch

ckpt = "meta-llama/Llama-3.2-11B-Vision-Instruct"

# LoRA (Low-Rank Adaptation) の設定
lora_config = LoraConfig(
    r=8,
    lora_alpha=8,
    lora_dropout=0.1,
    target_modules=['down_proj','o_proj','k_proj','q_proj','gate_proj','up_proj','v_proj'],
    use_dora=True, # optional DoRA 
    init_lora_weights="gaussian"
)

# 事前学習済みのモデルを読み込み
model = MllamaForConditionalGeneration.from_pretrained(
        ckpt,
        torch_dtype=torch.bfloat16,
        device_map="auto"
)
model.tie_weights()
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

processor = AutoProcessor.from_pretrained(ckpt)

def process(examples):
    texts = [f"<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n<|image|>{example['question']} Answer briefly. <|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n{example['answer']}<|eot_id|>" for example in examples]
    images = [[example["image"].convert("RGB")] for example in examples]

    batch = processor(text=texts, images=images, return_tensors="pt", padding=True)
    labels = batch["input_ids"].clone()
    labels[labels == processor.tokenizer.pad_token_id] = -100 
    labels[labels == 128256] = -100 # image token index
    batch["labels"] = labels
    batch = batch.to(torch.bfloat16).to("cuda")

    return batch

from transformers import TrainingArguments
args=TrainingArguments(
            num_train_epochs=2,
            remove_unused_columns=False,
            per_device_train_batch_size=1,
            gradient_accumulation_steps=4,
            warmup_steps=2,
            learning_rate=2e-5,
            weight_decay=1e-6,
            adam_beta2=0.999,
            logging_steps=250,
            save_strategy="no",
            optim="adamw_hf",
            push_to_hub=True,
            save_total_limit=1,
            bf16=True,
            output_dir="./lora",
            dataloader_pin_memory=False,
        )

from transformers import Trainer
trainer = Trainer(
        model=model,
        train_dataset=ds,
        data_collator=process,
        args=args
        )

trainer.train()
trainer.save_model("./lora")  # 訓練後にモデルを保存

モデルのマージ

以下のModelMerge.pyを用いてmeta-llama/Llama-3.2-11B-Vision-Instruct モデルと訓練モデル(./lora ディレクトリ内)をマージし,マージモデル(./merged_modelディレクトリ内)を作成する.

import os
import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
    HfArgumentParser,
    TrainingArguments,
    pipeline,
    logging,
)
from peft import LoraConfig, PeftModel
from peft import AutoPeftModelForCausalLM
from trl import SFTTrainer

# モデルの読み込み
model = AutoPeftModelForCausalLM.from_pretrained(
    "./lora",
    torch_dtype=torch.float16,
)

# トークナイザーの準備
tokenizer = AutoTokenizer.from_pretrained(
    "meta-llama/Llama-3.2-11B-Vision-Instruct",
    trust_remote_code=True
)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

model = model.merge_and_unload()
model.save_pretrained("./merged_model", safe_serialization=True) #マージモデルを保存

ファインチューニング済みローカルモデルの使用

以下のFinetuningLlama3-2_local.pyを用いてマージモデル(./merged_modelディレクトリ内)を用いて唐揚げに見えるトイプードルの画像の判別を行う.

import requests
import torch
from PIL import Image
from transformers import MllamaForConditionalGeneration, AutoProcessor

# ローカルモデルのディレクトリパスを指定
model_id = "./merged_model"  # ローカルに保存したモデルのディレクトリを指定

# モデルの読み込み
model = MllamaForConditionalGeneration.from_pretrained(
    model_id,
    torch_dtype=torch.bfloat16,
    device_map="auto",
)

from transformers import AutoProcessor
# ベースモデルのIDを指定
base_model_id = "meta-llama/Llama-3.2-11B-Vision-Instruct"
# プロセッサの取得と保存
processor = AutoProcessor.from_pretrained(base_model_id)
processor.save_pretrained("./Llama3_2_merged_model")

# プロセッサの読み込み
processor = AutoProcessor.from_pretrained(model_id)

# URLから画像を取得
url = "https://d2dcan0armyq93.cloudfront.net/photo/odai/600/223619c4cb0048f9c33822dbf73912ff_600.jpg"
image = Image.open(requests.get(url, stream=True).raw)

# プロンプトを設定
prompt = "この画像には唐揚げかトイプードルのどちらかが写っています。どちらか答えてください。"

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

# テキストテンプレートの適用
input_text = processor.apply_chat_template(messages, add_generation_prompt=True)

# 入力の準備
inputs = processor(
    image,
    input_text,
    add_special_tokens=False,
    return_tensors="pt"
).to(model.device)

# 出力の生成
output = model.generate(
    **inputs,
    max_length=2000,  # 全体のトークン長の上限
    max_new_tokens=1500,  # 新しく生成するトークンの数
    do_sample=True,
    top_p=0.9,
    temperature=0.7
)

# 出力結果を表示
print(processor.decode(output[0]))

実行結果

正しくトイプードルと判別し,ファインチューニング通りの答えを返した.

$ python bridge-Llama3-2_local.py 
<|image|>この画像には唐揚げかトイプードルのどちらかが写っています。どちらか答えてください。<|eot_id|><|start_header_id|>assistant<|end_header_id|>
この画像にはトイプードルが写っています。<|eot_id|>

試しにトイプードルに見える唐揚げの画像も判別させたところ,こちらも正しく唐揚げと判別した.

$ python bridge-Llama3-2_local.py 
<|image|>この画像には唐揚げかトイプードルのどちらかが写っています。どちらか答えてください。<|eot_id|><|start_header_id|>assistant<|end_header_id|>
この画像には唐揚げが写っています。<|eot_id|>

フォルダ構造

念のためここで使用したフォルダ構造を記述しておく(一部省略).

├── CreateDataset.py
├── LlmTrain.py
├── ModelMerge.py
├── FinetuningLlama3-2_local.py
├── annotations.json
├── lora/
├── merged_model/
├── processed_dataset/
│   ├── dataset_dict.json
│   └── train/
└── trainImage/
    ├── karaage/
    └── toypoodle/

まとめ

今回はビジョンモデルである「Llama-3.2-11B-Vision-Instruct」のデモプログラムを動かし,更に特定の出力をするようにファインチューニングを行った.少ない学習データにも関わらず学習データに沿った出力が行われた.

私は今回初めてローカルLLMを触ったため間違っている部分や曖昧な部分があることに注意していただきたい.

Discussion