• <xmp id="om0om">
  • <table id="om0om"><noscript id="om0om"></noscript></table>
  • Generative AI

    NeMo Framework で日本語 LLM をファインチューニング – PEFT 編 –

    Reading Time: 3 minutes

    ご注意: この記事は NeMo Framework のアップデートのため、2024 年 6 月 10 日に大幅に変更を加えました。

    本記事では、NeMo Framework を使用して、日本語の大規模言語モデル (LLM) の PEFT (ファインチューニングの手法の一種)を実行する方法を説明します。

    NeMo Framework とは

    NeMo Framework は、LLM をはじめ、生成 AI モデルを構築、カスタマイズするためのクラウドネイティブなフレームワークです。NGC 上にコンテナーが公開されており、すぐに利用を開始することができます。

    NeMo Framework は、NVIDIA AI Enterprise の対象ソフトウェアになっているため、エンタープライズ サポートを希望される場合は、NVIDIA AI Enterprise ライセンスの購入をご検討ください。

    LLM のワークフロー

    LLMの開発におけるタスクには以下のようなものがあります。

    • 事前學習に必要な大量データの準備
    • 分散學習を利用した LLM の事前學習
    • LLM をカスタマイズするためのファインチューニング (SFT, RLHF, PEFT) やプロンプト エンジニアリング
    • LLM の推論を高速化するためのモデル最適化
    • GPU を最大限に活用した LLM のサービング
    • コストを抑えながら LLM に最新情報を反映させるための RAG
    • LLM アプリケーションの意図しない挙動を抑えるためのガードレール

    LLM の開発、サービスでの利用には多くのステップが必要になりますが、NeMo Framework コンテナーには、データの準備から LLM の學習、カスタマイズに必要な下記モジュールが含まれており、これらを使用することで LLM の構築に関するステップを 1 つのコンテナー環境で実行できます。

    • NeMo Curator
      LLM の學習に必要な大規模データセットのダウンロードから抽出、クリーニング、フィルタリングなどを行うためのスケーラブルなツールキット。
    • NeMo
      LLM、マルチモーダル、音聲などの生成AIモデルを構築するためのスケーラブルなフレームワーク。
    • NeMo-Framework-Launcher
      クラウド、オンプレのクラスターからジョブを起動するためのツールキット。
    • Megatron-LM
      Transformer モデルの大規模學習に関する研究プロジェクト。
    • TransformerEngine
      FP8 を中心とした Transformer モデルを高速化させるツールキット。
    • NeMo-Aligner
      人間のフィードバックからの強化學習 (RLHF) 、DPO、SteerLMなどを使用して LLM を効率的にアライメントするためのツールキット。

    これらのライブラリは、GitHub 上に OpenSource として公開されていますが、依存関係が解消されている NeMo Framework コンテナーから利用することをお薦めします。コンテナーの場合、opt ディレクトリ配下に上記のモジュールが配置されています。

    PEFT とは

    PEFT (Parameter-Efficient Fine-Tuning) とは、LLM をカスタマイズする際に使われる手法の一種です。元の LLM に少數のパラメーターやレイヤーを追加し、ユース ケース固有のデータで學習を行います。元の LLM の重みは固定されたままであるため、學習中に更新されるパラメーターが大幅に少なくなります。

    これにより、LLM は全てのパラメーターを更新するファインチューニングと比較して、少ないコンピューティング リソースで特定のユース ケースでのパフォーマンスを改善できます。

    大規模言語モデルのカスタマイズ手法を選択するには、PEFT を含むカスタマイズ手法の解説があります。

    PEFT チュートリアル

    本記事では、Hugging Face Model Hub から日本語 LLM をダウンロードして、NeMo Framework を使用した PEFT を実行します。

    本チュートリアルでの手順は以下の通りです。

    • PEFT を実行するための事前準備
    • NeMo Framework コンテナーを起動
    • Hugging Face Model Hub から事前學習済みのモデルをダウンロード
    • ダウンロードしたモデルを nemo フォーマットへ変換
    • PEFT に使用するデータの準備および前処理
    • PEFT の実行
    • 推論

    事前準備

    • NGC から最新の NeMo Framework コンテナーを入手します (このチュートリアルでは、nvcr.io/nvidia/nemo:24.03.01.framework を使用しています)。
    • GPU が搭載されたマシンを準備します (このチュートリアルのコードは OS: Ubuntu 20.04.4 LTS, NVIDIA? A100 80GB × 1 の環境でテストしています)。Llama2 で PEFT を実行する際の HW 要件はこちらに記載があります。
    • 以下のコマンドで作業用のディレクトリを作成し、移動します。
    mkdir peft-example
    cd peft-example
    

    Docker コンテナーの起動

    以下のコマンドでコンテナーを起動します。

    sudo docker run --rm -it --gpus device=0 --shm-size=2g --ulimit memlock=-1 --network=host -v ${PWD}:/workspace  -w /workspace -v ${PWD}/results:/workspace/results nvcr.io/nvidia/nemo:24.03.01.framework bash
    

    Hugging Face Model Hub からモデルをダウンロード

    このチュートリアルでは、elyza/ELYZA-japanese-Llama-2-7b を使用します。以下のコードで Hugging Face の Model Hub から事前學習済みの LLM をダウンロードします。

    import os
    from huggingface_hub import snapshot_download
     
    MODEL_DIR = "./models"
    os.makedirs(MODEL_DIR, exist_ok=True)
     
    snapshot_download(
        repo_id="elyza/ELYZA-japanese-Llama-2-7b",
        local_dir=f"{MODEL_DIR}/ELYZA-japanese-Llama-2-7b",
        local_dir_use_symlinks=False
        )

    nemo フォーマットへの変換

    以下のスクリプトを使用して、ダウンロードした Hugging Face の Llama2 モデルを nemo フォーマットへ変換します。

    python /opt/NeMo/scripts/checkpoint_converters/convert_llama_hf_to_nemo.py --input_name_or_path=./models/ELYZA-japanese-Llama-2-7b --output_path=./models/ELYZA-japanese-Llama-2-7b/ELYZA-japanese-Llama-2-7b.nemo --precision="16"
    

    データの準備

    この PEFT チュートリアルでは JGLUE の JCommonsenseQA データセットを使用します。

    git clone https://github.com/yahoojapan/JGLUE.git

    以下のスクリプトを使用して、NeMo が PEFT で必要とする JSONL 形式へデータを変換します。ここでは検証用データを精度を確認するためのテスト データとして使用し、學習用データを分割することで學習および検証データとして使用します。

    import json
    import os
    import random
     
     
    INPUT_TRAIN = "./JGLUE/datasets/jcommonsenseqa-v1.1/train-v1.1.json"
    INPUT_VALID = "./JGLUE/datasets/jcommonsenseqa-v1.1/valid-v1.1.json"
    OUTPUT_DIR = "./data/jcommonsenseqa-v1.1"
     
     
    random.seed(42)
    os.makedirs(OUTPUT_DIR, exist_ok=True)
     
     
    def write_jsonl(fname, json_objs):
        with open(fname, 'wt') as f:
            for o in json_objs:
                f.write(json.dumps(o, ensure_ascii=False)+"\n")
     
     
    def form_question(obj):
        st = ""
        st += "### 指示:\n"
        st += "與えられた選択肢の中から、最適な答えを選んでください。出力は以下から選択してください:\n"
        st += f"- {obj['choice0']}\n"
        st += f"- {obj['choice1']}\n"
        st += f"- {obj['choice2']}\n"
        st += f"- {obj['choice3']}\n"
        st += f"- {obj['choice4']}\n"
        st += "### 入力:\n"
        st += f"{obj['question']}\n"
        st += "### 応答:"
        return st
     
     
    def prosess(input_path, train=False):
        with open(input_path) as f:
            dataset = [json.loads(line) for line in f.readlines()]
     
        processed = []
        for data in dataset:
            prompt = form_question(data)
            answer = data[f"choice{data['label']}"]
            processed.append({"input": prompt, "output": f"{answer}"})
    
        if train:
            random.shuffle(processed)
            train_ds = processed[:-1000]
            valid_ds = processed[-1000:]
            write_jsonl(f"{OUTPUT_DIR}/train-v1.1.jsonl", train_ds)
            write_jsonl(f"{OUTPUT_DIR}/valid-v1.1.jsonl", valid_ds)
        else:
            write_jsonl(f"{OUTPUT_DIR}/test-v1.1.jsonl", processed)
     
        return
     
     
    def main():
        prosess(INPUT_TRAIN, train=True)
        prosess(INPUT_VALID)
     
     
    if __name__ == "__main__":
        main()

    スクリプトが実行されると data というディレクトリの下に學習、検証、テストに使用するファイルが生成されます。変換後のデータを開いてみると以下のような形式になっています。

    {"input": "### 指示:\n與えられた選択肢の中から、最適な答えを選んでください。出力は以下から選択してください:\n- 貓\n- 家畜\n- 原宿\n- 牧草\n- 島根県\n### 入力:\n動物たちの餌になるものは?\n### 応答:", "output": "牧草"}
    {"input": "### 指示:\n與えられた選択肢の中から、最適な答えを選んでください。出力は以下から選択してください:\n- 巖手\n- 福島\n- 秋田\n- 愛知\n- 福井\n### 入力:\nきりたんぽが名物である県はどこか?\n### 応答:", "output": "秋田"}

    PEFT の実行

    PEFT は /opt/NeMo/examples/nlp/language_modeling/tuning/megatron_gpt_finetuning.py で実行できます。
    /opt/NeMo/examples/nlp/language_modeling/tuning/conf/megatron_gpt_finetuning_config.yaml に PEFT の実行に必要な config ファイルがあり、環境変數をセットすることでスクリプトの実行に必要なデータのパス、ベースとなる LLM (nemo フォーマット) のパスなどの設定を渡します (NeMo Framework は config 設定に Hydra を使用しています)。環境変數から設定されていないパラメーターは config ファイルの設定が適用されます。実行コマンドにある ++model.mcore_gpt=True は Hydra で config の追加または上書きをする際の記法になります。

    現在、NeMo Framework では以下の PEFT の手法をサポートしており、SCHEME を変更することで他の手法を実行できます。以下は P-Tuning を選択した場合で説明します。PEFT とモデルのサポートリストはこちらにあります。

    • P-Tuning
    • Adapter
    • IA3
    • LoRA
    export EXP_DIR="/workspace/results"
    export EXP_NAME="elyza_7b_ptuning"
    export MODEL="/workspace/models/ELYZA-japanese-Llama-2-7b/ELYZA-japanese-Llama-2-7b.nemo"
    export SCHEME="ptuning"
    export TP_SIZE=1
    export PP_SIZE=1
    export TRAIN_DS="[/workspace/data/jcommonsenseqa-v1.1/train-v1.1.jsonl]"
    export VALID_DS="[/workspace/data/jcommonsenseqa-v1.1/valid-v1.1.jsonl]"
    
    
    torchrun --nproc_per_node=1 \
    /opt/NeMo/examples/nlp/language_modeling/tuning/megatron_gpt_finetuning.py \
        exp_manager.exp_dir=${EXP_DIR} \
        exp_manager.name=${EXP_NAME} \
        exp_manager.early_stopping_callback_params.patience=5 \
        trainer.precision=bf16 \
        trainer.devices=1 \
        trainer.num_nodes=1 \
        trainer.max_epochs=-1 \
        trainer.max_steps=10000 \
        trainer.val_check_interval=0.05 \
        trainer.gradient_clip_val=1.0 \
        model.megatron_amp_O2=False \
        ++model.mcore_gpt=True \
        model.restore_from_path=${MODEL} \
        model.peft.peft_scheme=${SCHEME} \
        model.data.train_ds.file_names=${TRAIN_DS} \
        model.data.train_ds.concat_sampling_probabilities=[1.0] \
        model.data.validation_ds.file_names=${VALID_DS} \
        model.tensor_model_parallel_size=${TP_SIZE} \
        model.pipeline_model_parallel_size=${PP_SIZE} \
        model.global_batch_size=16 \
        model.micro_batch_size=16 \
        model.data.validation_ds.tokens_to_generate=10 \
        model.data.validation_ds.metric.name="loss" \
        model.data.train_ds.num_workers=0 \
        model.data.validation_ds.num_workers=0 \
        model.optim.lr=1e-5

    NeMo Frameworkでは、実験管理に Weights and Biases をサポートしており、 wandb へログイン後に以下のコマンドを追加して、スクリプトを実行することで wandb 上で実験管理することができます。

    exp_manager.create_wandb_logger=True \
    exp_manager.wandb_logger_kwargs.project=${WANDB_PROJECT} \
    exp_manager.wandb_logger_kwargs.name=${WANDB_NAME} \

    上記の設定では、3,000 ステップほどで改善が止まり、その後、Early stopping がかかったため、學習は 1 時間ほどで完了しました。

    學習が終わると results/elyza_7b_ptuning/ という名前のディレクトリが作成され、中に學習時の config や log などが出力されます。また、同じディレクトリの checkpoints 內にある elyza_7b_ptuning.nemo が PEFT で學習されたパラメーターの checkpoint ファイルになります。

    推論

    學習が完了したら、/opt/NeMo/examples/nlp/language_modeling/tuning/megatron_gpt_generate.py を使用して、テスト データで推論を実行します。model.restore_from_path には先ほどの PEFT でベースとなったモデルのパス、model.peft.restore_from_path には先ほど PEFT を実行して作成されたモデルのパスを渡します。model.data.test_ds.write_predictions_to_file を True に設定すると入力ファイルに推論結果が追加されたファイルが出力されます。

    export MODEL="/workspace/models/ELYZA-japanese-Llama-2-7b/ELYZA-japanese-Llama-2-7b.nemo"
    export PEFT_MODEL="/workspace/results/elyza_7b_ptuning/checkpoints/elyza_7b_ptuning.nemo"
    export TEST_NAMES="[jcommonsenseqa-v1.1]"
    export TEST_DS="[/workspace/data/jcommonsenseqa-v1.1/test-v1.1.jsonl]"
    export OUTPUT_PREFIX="/workspace/results/elyza_7b_ptuning"
    export NVTE_FLASH_ATTN=0
    export NVTE_FUSED_ATTN=0
     
     
    python /opt/NeMo/examples/nlp/language_modeling/tuning/megatron_gpt_generate.py \
        model.restore_from_path=${MODEL} \
        model.peft.restore_from_path=${PEFT_MODEL} \
        trainer.devices=1 \
        model.data.test_ds.file_names=${TEST_DS} \
        model.data.test_ds.names=${TEST_NAMES} \
        model.data.test_ds.global_batch_size=16 \
        model.data.test_ds.micro_batch_size=16 \
        model.data.test_ds.tokens_to_generate=10 \
        inference.greedy=True \
        model.data.test_ds.output_file_path_prefix=${OUTPUT_PREFIX} \
        model.data.test_ds.write_predictions_to_file=True

    上記設定で推論が完了すると results の下に elyza_7b_ptuning_test_jcommonsenseqa-v1.1_inputs_preds_labels.jsonl というファイルが出力されます。

    以下に Few-shot(2-shot) での一例を示します。

    # 以下はFew-shotでのプロンプトと出力
    
    ### 指示:
    與えられた選択肢の中から、最適な答えを選んでください。出力は以下から選択してください:
    ### 例題1:
    - 世界
    - 寫真集
    - 絵本
    - 論文
    - 図鑑
    ### 入力:
    主に子ども向けのもので、イラストのついた物語が書かれているものはどれ?
    ### 応答: 絵本
    ### 例題2:
    - 浮浪者
    - 保護者
    - お坊さん
    - 宗教者
    - 預言者
    ### 入力:
    未成年者を監護?教育し,彼らを監督し,彼らの財産上の利益を守る法律上の義務をもつ人は?
    ### 応答: 保護者
    ### 本題:
    - 掲示板
    - パソコン
    - マザーボード
    - ハードディスク
    - まな板
    ### 入力:
    電子機器で使用される最も主要な電子回路基板の事をなんと言う?
    ### 応答:
    マザーボード
    ###
    

    次に P-Tuning 後の出力の例を示します。

    # 以下はP-Tuning後のプロンプトと出力
    
    ### 指示:
    與えられた選択肢の中から、最適な答えを選んでください。出力は以下から選択してください:
    - 掲示板
    - パソコン
    - マザーボード
    - ハードディスク
    - まな板
    ### 入力:
    電子機器で使用される最も主要な電子回路基板の事をなんと言う?
    ### 応答:
    マザーボード
    

    Few-shot ではプロンプトの設定により、出力に対して、改行コードの前までを取り出すなどの処理が必要になりますが、P-Tuning 後では答えのみが出力されるようになりました。また、全體の正解率も P-Tuning 後の方が優れた結果が得られていることを確認しています。

    まとめ

    本記事では、NeMo Framework を使用した PEFT の実行方法を紹介しました。NeMo Framework を使用して LLM の開発が加速すると嬉しいです。

    関連情報

    +8

    Tags

    人人超碰97caoporen国产