⚕️

Alpaca-LoRAをPubMedQAでファインチューニングしてみる

2023/04/19に公開

はじめに

こんにちは、CareNetのKaiと申します。
CareNetは医療従事者向けメディアを運営している上場企業でして、遅ればせながらテックブログを始めようということで、こちらが最初の記事になります。

最近CareNetでも機械学習チームが立ち上がっており、LLM(大規模言語モデル)を使ったプロダクト開発に取り組んだりしています。

今回は、LLMのうちでも計算量・VRAMを削減することのできるAlpacaとLoRAの組み合わせに対して、医療分野でのQ&AデータセットであるPubMedQAでファインチューニングしてみましたので、ご参考になれば幸いです。

AlpacaとLoRA

この記事にたどり着いている時点で解説不要な気はしますが、前提としてざっくり記載しておきますね。

ご存知の通り、LLMはChatGPTで広く話題になり、史上最速で1億ユーザに到達したとも言われています。
とはいえ、ある日突然産まれた発明というわけではなく、ChatGPTより遥か前から様々な形で研究されてきたものが、ついに花開いたと考えられます。

ChatGPTに用いられているアルゴリズムの原理は、「GPT(Generative Pretrained Transformer)」の名前が示す通り、Transformerというものです。

これは、「Attention Is All You Need」という妙にエモいタイトルの論文で発表されたもので、現在のLLMはほとんどがこのTransformerをベースに作られています。
(Transformer以前の、伝統的なRNNでもブレイクスルーが起きており、Transformerベースに匹敵するRWKVというものも最近提案されています)
https://dev.classmethod.jp/articles/try-rwkv/

さて、Alpacaとは、スタンフォード大学が公開しているLLMであり、Metaが公開しているLLaMAをOpenAIのtext-davinci-003で作成したデータセットでファインチューニングしたものです。

注意しなければいけないのは、Alpacaは上記の通りMetaとOpenAIのアセットを利用しているため、その利用条件・規約に縛られるということです。研究目的、非商用目的での利用のみが許可されています。

本記事での利用例も、プライベート環境で実施している研究の一部を抽出したもので、これを商用利用・営利利用するものではないことを記載しておきます。

次にLoRAですが、ご存知の通りLLMのトレーニングやファインチューニングには、莫大なGPUリソースが必要となります。特にVRAMの容量がボトルネックですね。
これを解決するために様々な軽量化手法が提案されており、その中の一つがMicrosoftの提案したLoRA(Low-Rank Adaption of Large Language Models)です。

LoRAについては、以下の記事が詳しいのでご興味のある方はどうぞ。

https://engineering.linecorp.com/ja/blog/lora-tuning-for-japanese-model

私見ですが、GPT-4に匹敵するような超大規模モデルに取り組める組織は限られているので、言語モデル研究の中心はLoRAを始めとする軽量化手法に移っていくのではないかと考えています。

PubMedQA

次に、ファインチューニングに利用したデータセットであるPubMedQAについて紹介します。
医療系の業界にいらっしゃらない方は馴染みのない名前かもしれませんが、PubMedとはアメリカ国立医学図書館が運営している、世界最大規模の生物化学・生物医学の文献データベースであり、誰でも無料で利用することができます。

このPubMedに収録されている抄録から、QAタスク(YES/NO/MAYBEで答えられるような質問と回答のセット)を作成したものが、PubMedQAです。

データセットとリーダーボードはこちらで公開されています。

https://pubmedqa.github.io/index.html

今回は、このPubMedQAのデータセットを用いて、Alpaca-LoRAをファインチューニングしてみます。

Colabでの実装とファインチューニング

データセットの前処理

PubMedQAのデータセットは、ファイルによって多少構成が異なるものの、以下のようなフォーマットになっています。今回はお試しなので、人工的に生成され、YES/NOのいずれかの答えになる「ori_pqaa.json」を使いました。これは最もデータ量が多く、20万件以上のQAがあります。
(PQAAは、結論などの文章から自動的に生成されたQAなので、ひょっとしたら微妙なミスがある可能性はあります)

    "ID": {
        "QUESTION": "Are group 2 innate lymphoid cells ( ILC2s ) increased in chronic rhinosinusitis with nasal polyps or eosinophilia?",
        "CONTEXTS": [
            "",
            "",
            ""
        ],
        "LABELS": [
            "",
            "",
            ""
        ],
        "LONG_ANSWER": "",
        "MESHES": [
            "",
            ""
        ],
        "final_decision": "yes"
    }

そして、Alpaca-LoRAのファインチューニングに必要なフォーマットは以下のようなものです。

{
    "instruction": "Please answer YES/NO to the medical questions presented.",
    "input": "Are group 2 innate lymphoid cells ( ILC2s ) increased in chronic rhinosinusitis with nasal polyps or eosinophilia?",
    "output": "yes"
}

したがって、以下のコードでPubMedQAのデータセットをファインチューニング用のデータセットに変換します。

import json

# 元ファイルを読み込み
with open('インプットファイル名', 'r') as f:
    data = json.load(f)

# 新しいデータを格納するリストを作成
new_data_list = []

# 複数のIDを処理
for id in data:
    # 必要な情報を取得して、新しいデータ構造に変換し、リストに追加
    new_data = {
        "instruction": "Please answer YES/NO to the medical questions presented.",
        "input": data[id]["QUESTION"],
        "output": data[id]["final_decision"]
    }
    new_data_list.append(new_data)

# 新しいファイルに書き出し
with open('アウトプットファイル名', 'w') as f:
    json.dump(new_data_list, f)

これで、ファインチューニング用の約45MBのデータセットが出来ました。

ファインチューニング

ファインチューニングはGoogle Colab上で行います。
ProライセンスのプレミアムGPUで動かすことが出来ました。エポック数、バッチサイズ等を調整して、6時間ちょっとで終わるように調整してあります。

まずは前準備から。

!git clone https://github.com/tloen/alpaca-lora.git
%cd alpaca-lora/
!pip install -r requirements.txt

gradioをColabで使えるようにするため、設定を書き換えます。

!cat -n generate.py | tail -n 30
 195	    ).queue().launch(server_name="0.0.0.0", share=share_gradio)

195行目に"share=share_gradio"という部分があるので、これを"share=True"に書き換えます。

!sed -i -e "195s/share_gradio/True/" generate.py

これで、ブラウザ上で動作確認することができます。

では、先ほどのデータセットを用いてファインチューニングしてみます。
とはいっても、ほとんどスタンフォードのリポジトリで例示されている元のコマンドそのままで、Colabの時間制限に引っかからないようにしています。
お試しなのでエポックは1回にしています。

!python finetune.py \
    --base_model 'decapoda-research/llama-7b-hf' \
    --data_path '先ほどのファインチューニングファイル' \
    --output_dir 'モデルの保存先' \
    --batch_size 256 \
    --micro_batch_size 8 \
    --num_epochs 1 \
    --learning_rate 1e-4 \
    --cutoff_len 512 \
    --val_set_size 2000 \
    --lora_r 8 \
    --lora_alpha 16 \
    --lora_dropout 0.05 \
    --lora_target_modules '[q_proj,v_proj]' \
    --train_on_inputs \
    --group_by_length

ファインチューニングはおおむね6時間ちょっとで終了し、lossは以下のような形でした。
まぁある程度は収束していますね。
ちなみにVRAM消費量は10GBちょっとでしたので、RTX3060など12GBのVRAM搭載グラボなら動きそうな感じです。

{'loss': 3.0556, 'learning_rate': 1e-05, 'epoch': 0.01}
{'loss': 0.6945, 'learning_rate': 1.2552301255230125e-06, 'epoch': 0.99} 

FT結果

CareNetでは、メディア上で日替わりの○×の臨床クイズを掲載しています。
そのクイズを試しに10問解いてもらいました。

質問No 正解 Alpaca-LoRA(FT前) Alpaca-LoRA(FT後) ChatGPT-3.5turbo(参考)
1 NO NO YES NO
2 NO NO YES NO
3 YES NO YES YES
4 NO YES YES YES
5 NO NO YES YES
6 YES NO YES YES
7 YES NO YES NO
8 NO YES YES YES
9 NO NO YES NO
10 YES NO YES YES

FT前のAlpaca-LoRAは正解率40%、ChatGPT-3.5turboは正解率50%で、ほぼこれはランダム回答と変わらない感じですね。

FT後のものは、全てにYESを返すBotになっているように見受けられます。原論文にあたってみると、PQAAで自動生成された回答の92.8%がYESだったようです。
これは論文等の抄訳から自動生成したQA(NO回答は否定文からのみ生成される)なのでやむを得ないのですが、このデータセットでは全てにYESを返すBotが92.8%の精度を得られてしまうということですね。

おわりに

結局、お試しですがファインチューニングした結果、全てにYESを返すBotが出来上がってしまいました。
これは図らずも、学習データの特性や統計量を考慮に入れなければ結果が得られないという、"Garbage In ,Garabage Out"の好例だと考えられます。

改善する方法はいくつか考えられますが、ぱっと思いつくのは以下でしょうか。

  • データ量がかなり多いので、YESの回答となる例をNOと同数だけ抽出して、YES/NOが均衡したデータセットを作る
  • 損失関数を見直して、不均衡データに対応した形に書き換える

不均衡データは、機械学習を回すときに必ずついて回る問題ですし、対処法はデータの統計量や取り組むタスクによって変わってくるので、個別に検討していく必要があります。

こちらの記事にまとまった情報が載っていますので、ご興味のある方はどうぞ。

https://qiita.com/koshian2/items/52e4bdc682fc152fe05e

参考文献・リンク

https://note.com/masa_kazama/n/nabaa6dfec741
https://note.com/npaka/n/n1a0ab681dc70
https://qiita.com/toshi_456/items/280efc31950ddb083286
https://zenn.dev/kurehajime/scraps/526acef3b2117f

CareNet Engineers

Discussion