📑

Unslothを用いたQwen2.5 VLのCPT&SFTによる運転免許証KIEタスクの検証

に公開

はじめに

業務でLLMを活用していくうえで、どうしても「精度・コスト・速度」のバランスを取る必要があります。特に、各社の提供する高性能なLLMは非常に優れた精度を持っている一方で、コストやレイテンシの面では課題を感じることも多いです。また、学習するにしても、高額なGPUを使用する必要があり、学習コストの面でも課題があります。

そこで今回注目したのが、小型のLLMを使って特定タスクに特化した形でファインチューニングするアプローチです。このアプローチによって、限られたリソース環境でも十分な精度を保ちつつ、学習コスト、や推論速度(レイテンシ)を抑えることができる可能性があります。

低リソース環境でも効率的に学習・推論が行える点から、今回注目したのが、軽量かつ高速な学習が特徴の Unsloth というライブラリです。今回はこのUnslothを使って、LoRAによる継続事前学習とSFT(教師ありファインチューニング)を試し、どの程度の精度や推論速度が出るのかを検証してみました。

検証タスクとしては、実務でもよくあるようなケースを想定し、運転免許証のKIE(Key Information Extraction)を簡易的に再現したタスクを用いています。

Unslothとは

Unslothは、Hugging FaceのTransformersライブラリと高い互換性を保ちつつ、大規模言語モデル(LLM)の継続事前学習(Continual Pre-training)や教師ありファインチューニング(SFT)を高速かつ軽量に行えるライブラリです。

LoRA(Low-Rank Adaptation)による効率的なパラメータ更新に加え、Flash Attentionや8bit/4bit量子化といった最新の最適化技術を統合することで、低リソース環境でも高スループットと安定性を実現します。

特に、Unslothが提供するFastLanguageModelやFastVisionModelは、Transformersと同様のAPIで扱える一方、メモリ効率やトレーニング速度が大幅に向上しています。SFTに加え、継続的事前学習やマルチモーダルデータへの対応も進められており、実験用途からプロダクション用途まで幅広い活用が期待されています。

https://github.com/unslothai/unsloth

Qwen2.5-VLとは

Qwen2.5-VL は、2024/8/30に公開されたAlibaba CloudのQwenシリーズにおけるマルチモーダル(視覚・言語統合)モデルです。視覚認識・ドキュメント解析・動画理解の分野において、GPT-4oやClaude3.5 sonnetと比較して高い性能を実現しています。

🌟 特徴としては、
・画像・文書・動画をネイティブに理解
・動的解像度処理と時間情報の扱いに対応
・オブジェクト位置の高精度な特定(バウンディングボックス・ポイント)
・請求書・表・図表など構造化データの抽出に強い
・今回のKIEタスクでは、この特徴が重要!!
・数時間におよぶ動画内イベントの秒単位特定も可能
https://huggingface.co/papers/2502.13923

運転免許証KIE(Key Information Extraction)タスク

運転免許証KIEタスクでは、運転免許に記載の名前(name)、住所(adddress)、生年月日(birthday)、交付日(issue_date)、有効期限(expire_date)、免許証番号(license_number)を抽出することを目的とする。

データ生成

実際の運転免許証を大量に収集することは、個人情報を扱う観点から現実的に難しいため、簡易的な運転免許証を自動生成して、学習データ100件と検証データ20件を作成する。なお名前と住所に関しては、学習時と検証時で完全に異なる文字列生成することで、学習時に獲得した単なる文字列の並びによる推論ではなく、画像から該当の文字列を抽出できるか否かを検証できるように自動生成する。


生成した画像例

上記の抽出例:

{
    "name": "山田 太郎",
    "address": "東京都中央区日本橋本町三丁目8番3号",
    "birthday": "平成25年12月1日",
    "issue_date": "令和12年12月09日",
    "expire_date": "2031年12月1日",
    "license_number": "200303543000"
}

実行環境&モデル

CPT(Continual Pre-training)

Unslothが提供するNotebookとOfficial Documentの設定を参考に各種パラメータを設定した。

model = FastVisionModel.get_peft_model(
    model,
    finetune_vision_layers     = True, # False if not finetuning vision layers
    finetune_language_layers   = True, # False if not finetuning language layers
    finetune_attention_modules = True, # False if not finetuning attention layers
    finetune_mlp_modules       = True, # False if not finetuning MLP layers

    r = 16 or 128,           # The larger, the higher the accuracy, but might overfit
    lora_alpha = 128,  # Recommended alpha == r at least
    lora_dropout = 0,
    bias = "none",
    random_state = 3407,
    use_rslora = False,  # We support rank stabilized LoRA
    loftq_config = None, # And LoftQ
)
trainer = UnslothTrainer(
    model = model,
    tokenizer = tokenizer,
    data_collator = UnslothVisionDataCollator(model, tokenizer),
    train_dataset = converted_train_dataset,
    args = UnslothTrainingArguments(
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 4,
        warmup_steps = 5,
        num_train_epochs = 3, # 本検証の経験則に基づき3を設定
        learning_rate = 5e-5,
        embedding_learning_rate = 1e-5,
        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",
        remove_unused_columns = False,
        dataset_text_field = "",
        dataset_kwargs = {"skip_prepare_dataset": True},
        dataset_num_proc = 4,
        max_seq_length = 4096,
    ),
)

https://docs.unsloth.ai/basics/continued-pretraining

SFT(Supervised Fine-Tuning)

こちらも同様にUnslothが提供するNotebookとOfficial Documentの設定を参考に各種パラメータを設定した。

model = FastVisionModel.get_peft_model(
    model,
    finetune_vision_layers     = True, # False if not finetuning vision layers
    finetune_language_layers   = True, # False if not finetuning language layers
    finetune_attention_modules = True, # False if not finetuning attention layers
    finetune_mlp_modules       = True, # False if not finetuning MLP layers

    r = 16 or 128,           # The larger, the higher the accuracy, but might overfit
    lora_alpha = 16 or 128,  # Recommended alpha == r at least
    lora_dropout = 0,
    bias = "none",
    random_state = 3407,
    use_rslora = False,  # We support rank stabilized LoRA
    loftq_config = None, # And LoftQ
)
trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    data_collator = UnslothVisionDataCollator(model, tokenizer),
    train_dataset = converted_train_dataset,
    args = SFTConfig(
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 4,
        warmup_steps = 5,
        num_train_epochs = 3, # 本検証の経験則に基づき3を設定
        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",
        remove_unused_columns = False,
        dataset_text_field = "",
        dataset_kwargs = {"skip_prepare_dataset": True},
        dataset_num_proc = 4,
        max_seq_length = 4096,
    ),
)

https://docs.unsloth.ai/basics/vision-fine-tuning

精度検証

再現性を持たせるため、greedy decodingとなるような推論パラメータで、各項目の文字列の完全一致精度(Accuracy)を計測した。

  • 推論パラメータ
    • temperature: 0.0
    • min_p: 0.1
    • max_tokens: 256
    • max_model_len: 4096
    • dtype: float16
検証名 name address birthday license_number issue_date expire_date Average
base 65.0±5.0 100.0±0.0 100.0±0.0 100.0±0.0 100.0±0.0 0.0±0.0 77.5±0.8
SFT(lora_alpha=16) 83.3±2.9 95.0±0.0 100.0±0.0 100.0±0.0 100.0±0.0 100.0±0.0 96.4±0.5
SFT(lora_alpha=128) 90.0±0.0 100.0±0.0 95.0±0.0 100.0±0.0 100.0±0.0 23.3±2.9 84.7±0.5
CPT(lora_alpha=128) 76.7±2.9 95.0±0.0 100.0±0.0 81.7±2.9 100.0±0.0 75.0±5.0 88.1±0.5
CPT + SFT(lora_alpha=128) 88.3±2.9 100.0±0.0 100.0±0.0 90.0±0.0 100.0±0.0 100.0±0.0 96.4±0.5

※ レスポンスが変動するため、3回の測定値の平均値±標準偏差を記載
※ 因みにlora_alpha=16でnum_epochが少ないと、破滅的忘却に陥る傾向が見られた

baseのモデルでも既に精度が高いものの、nameの姓と名の間のスペースが曖昧だったり、特にexpire_dateの取得元が、birthdayやissue_dateから取得する傾向が見られ、この点が課題となった。
今回の検証環境においては、AverageのAccuracyを指標とした場合、SFT(lora_alpha=16)とCPT + SFT(lora_alpha=128)で最も高い精度を示し、baseモデルでの課題となっていたexpire_dateの取得元の学習が進み、対応できるようになった。

パフォーマンス検証

unslothのFastVisionModel.for_inference()で推論最適化を行なっても、レイテンシはそれほど早くないと感じたため、HuggingFace TransformersまたvLLMのいくつかのパターンで、レイテンシおよびピークメモリの測定を行った。
HuggingFace Transformersでは、メモリ使用量の削減を目的にbitsandbytesによる4bit量子化(bnb4)を適用した。また、Attention層の計算を高速化するためにFlashAttention2も併用した構成について検証を行った。

  • モデル:Qwen/Qwen2.5-VL-3B-Instruct
    • UnslothでCPT+SFTしたモデル
  • 実行環境
    • GPU: NVIDIA A100
    • Dockerコンテナ:nvidia/cuda:12.6.0-devel-ubuntu22.04
    • torch==2.7.0, transformers==4.52.4, flash_attn==2.8.0.post2
  • 推論パラメータ
    • max_tokens: 256
    • max_model_len: 4096
    • temperature: 0.0
    • dtype: float16 or bfloat16
推論フレームワーク レイテンシ(s) ピークメモリ(GiB)
HuggingFace Transformers(float16) 約 4.31 ± 0.37 7.6
HuggingFace Transformers(bfloat16) 約 4.28 ± 0.29 7.6
HuggingFace Transformers(float16) + flash-attention2 約 4.23 ± 0.28 7.6
HuggingFace Transformers(bfloat16) + flash-attention2 約 4.24 ± 0.31 7.6
HuggingFace Transformers(float16) + bnb4 + flash-attention2 約 6.56 ± 0.42 3.8
HuggingFace Transformers(bfloat16) + bnb4 + flash-attention2 約 5.44 ± 0.56 3.8
vLLM(float16) 約 1.25 ± 0.02 15.1(vLLM割り当てメモリ)
vLLM(bfloat16) 約 1.43 ± 0.95 15.1(vLLM割り当てメモリ)

まとめ

Unslothを活用することで、Google Colab T4環境(無料枠)でCPTおよびSFTを実行することが可能であった。(実際のところ、Google Colab T4のRAMの1/2程度までしか使用していなかった。)
また、簡易的な運転免許証KIEタスクにおいて、CPTおよびSFTすることで、精度向上が確認できた。
推論時のレイテンシにおいても、vLLMを使用することで、16GiBの範囲内で1s台のレイテンシで高速に推論することが可能であった。

今回は小規模の合成データで学習を行ったが、実タスクにおいては、多くの実データを用いたデータバリエーションでCPTおよびSFTすることで、実タスクで運用可能な精度の小規模特化型モデルを作成する1つの手法になりうると思われた。

また、運用コストを考えると、例えば、AWS g4dn.xlarge(16GiB)インスタンスを東京リージョンで立ち上げた場合、1ヶ月のコストは2025/6現在のレートで74,160円(30day × 24h × $0.71/h × 144円/ドル)となる。1ヶ月のうち80%稼働しているとした場合、2,073,600 秒(30day × 24h × 60m × 60s × 80%)稼働していることとなり、vLLMで推論APIを実行している場合、多めに見積もってレイテンシが1.5s/requestとして1,382,400リクエストを捌くことができる。つまり、1リクエスト0.054円(74,160円 / 1,382,400)と破格の値段で推論できる。バッチ処理にしたら、さらにコスト減が見込める。
コスト面を考えると、これからは業務向けの小規模LLMを作る動きがもっと増えてきそうだ。

参考

https://github.com/unslothai/unsloth
https://huggingface.co/papers/2502.13923
https://instances.vantage.sh/aws/ec2/g4dn.xlarge?region=ap-northeast-1

ELEMENTS Engineering

Discussion