Skip to content

DNET_HFTRLSFTTrainer

nishi_74322014 edited this page Sep 11, 2026 · 1 revision

huggingface/trl SFTTrainer

概要

huggingface/trl SFTTrainer

huggingface/trl

  • Hugging Faceが提供するtransformersの拡張
  • Transformer Reinforcement Learningの略で指示・強化学習(LLMのFTの該当節を参照)の機能を提供する。

SFTTrainer

trlに含まれるトレーナー・クラス

  • 教師ありファインチューニング(SFT)に特化したトレーナー。
  • Trainerクラスを継承していて、SFT向けの機能を追加している。

SFTTrainerでできること

  • CLM(Causal Language Modeling)による学習(次単語を予測するタスク):data_collatorを明示的に指定しない場合

    collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
    
  • MLM(Masked Language Modeling)による学習(マスクを予測するタスク):BERTのような双方向モデルが前提

    collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=True)
    
  • Instruction Tuning:formatting_func、data_collatorをInstruction Tuning用に設定

    collator = DataCollatorForCompletionOnlyLM(response_template, tokenizer=tokenizer)
    
  • LoRAによるチューニング

  • packingによる学習

  • NEFTuneによる学習

詳細

実装準備

インストール

  • 領域が足りないことがあるので注意

    • 「pip cache purge」&「TMPDIR=/mnt/tmp pip install」を使用
    • 必要に応じてディスク追加(OSSのLLMの該当節を参照)を行う。
  • ModuleNotFoundError: No module named '_lzma'

プロキシ環境(Pythonの該当節を参照)

データセット

  • kunishou/databricks-dolly-15k-ja

    from datasets import load_dataset
    
    dolly_dataset = load_dataset("kunishou/databricks-dolly-15k-ja")
    
    # 簡易化のためinputの値が空のデータに絞る
    # npakaさんの記事(https://note.com/npaka/n/nc7a4c20f98f8)を参考
    dolly_train_dataset = dolly_dataset['train'].filter(lambda example: example['input'] == '')
    
    print(dolly_train_dataset)
    # データ件数は全部で10,417件
    #Dataset({
    #    features: ['output', 'index', 'category', 'instruction', 'input'],
    #    num_rows: 10417
    #})
    
    print(dolly_train_dataset[0])
    #{'output': 'イコクエイラクブカ',
    # 'index': '1',
    # 'category': 'classification',
    # 'instruction': '魚の種類はどっち?イコクエイラクブカとロープ',
    # 'input': ''}
    
  • 自作JSON(jsonl)

    • ロード

      train_file = "data/train.jsonl"
      dataset = datasets.load_dataset("json", data_files=train_file, split="train")
      dataset = datasets.DatasetDict({'train': dataset})
      
    • JSONL
      各行が個別の JSON オブジェクトとして扱われるファイル形式

      {"answer":"XXX","question":"YYY","context":"ZZZ"}
      {"answer":"XXX","question":"YYY","context":"ZZZ"}
      {"answer":"XXX","question":"YYY","context":"ZZZ"}
      ...
      
    • split="train"を書いた場合と書かない場合の話
      多くのHugging FaceのAPIやTrainerなどは Dataset オブジェクトを DatasetDict に格納する構造(例:{'train': Dataset, 'validation': Dataset})を想定している。

      • 書いた場合
        split="train" を指定することで、datasets.load_dataset() は Dataset 型を返し、それを明示的に DatasetDict にtrainと言うキー名でラップしているため、正しい構造になる。

        DatasetDict({
            'train': Dataset(...)
        })
        
      • 書かない場合
        split を指定しない場合、戻り値は DatasetDict 型になり、これは、自動的に "train" というキーを持つ辞書形式になる。
        その後、再度 DatasetDict にtrainと言うキー名でラップしているため、入れ子になった DatasetDict になる。コレは間違った構造。

        DatasetDict({
            'train': DatasetDict({
                'train': Dataset(...)
            })
        })
        

※ kunishou/databricks-dolly-15k-jaを使う例ではcontextを使用しないのでinput(≒context)の値が空のデータに絞っている。

モデル

  • モデルとトークナイザ(文字列と数値IDを相互変換する道具)

  • 2つの例を見ると、オプションに違いがあるが、コレは? | オプション | 説明 | | --- | --- | | device_map="auto" | GPUやCPUへ自動で重みを割り当て | | torch_dtype=torch.bfloat16 | 省メモリ・高速化のためのデータ型(bfloat16)を明示 | | trust_remote_code=True | モデル定義にカスタムコードが含まれる場合に許可 | | force_download=True | キャッシュ無視して強制的に再取得 |

  • cyberagent/open-calm-large

    • ダウンロード

      from transformers import AutoModelForCausalLM, AutoTokenizer
      # open-calm-largeは約7億パラメータの小さめのLLMです。
      # とはいえ、本記事で紹介するコードをそのまま使うとcolab pro+で使えるA100ギリギリかもしれません。(試してません)
      # お手元のGPUのリソースが限られている場合、動作確認が目的であれば、open-calm-smallに切り替えるなどしていただくと良いかもしれません。
      model_name = "cyberagent/open-calm-large"
      tokenizer = AutoTokenizer.from_pretrained(model_name)
      model = AutoModelForCausalLM.from_pretrained(
          model_name,
          device_map="auto",
          # torch_dtype=torch.float16,
          # torch.float16を指定すると、train時のlossが0.0になって学習がうまくいかない。
          # TrainerArgumentsを修正(bf16 = True)したところ正常に評価できる(TPUで事前学習したモデル)。
      

)

  • ロード

    save_pretrainedしていれば、model = AutoModelForCausalLM.from_pretrained('./output', device_map="auto")でロードできる。
    
  • print(model)の出力

    GPTNeoXForCausalLM(
      (gpt_neox): GPTNeoXModel(
        (embed_in): Embedding(52096, 1536)
        (emb_dropout): Dropout(p=0.0, inplace=False)
        (layers): ModuleList(
          (0-23): 24 x GPTNeoXLayer(
            (input_layernorm): LayerNorm((1536,), eps=1e-05, elementwise_affine=True)
            (post_attention_layernorm): LayerNorm((1536,), eps=1e-05, elementwise_affine=True)
            (post_attention_dropout): Dropout(p=0.0, inplace=False)
            (post_mlp_dropout): Dropout(p=0.0, inplace=False)
            (attention): GPTNeoXAttention(
              (rotary_emb): GPTNeoXRotaryEmbedding()
              (query_key_value): Linear(in_features=1536, out_features=4608, bias=True)
              (dense): Linear(in_features=1536, out_features=1536, bias=True)
              (attention_dropout): Dropout(p=0.0, inplace=False)
            )
            (mlp): GPTNeoXMLP(
              (dense_h_to_4h): Linear(in_features=1536, out_features=6144, bias=True)
              (dense_4h_to_h): Linear(in_features=6144, out_features=1536, bias=True)
              (act): GELUActivation()
            )
          )
        )
        (final_layer_norm): LayerNorm((1536,), eps=1e-05, elementwise_affine=True)
      )
      (embed_out): Linear(in_features=1536, out_features=52096, bias=False)
    )
    
  • Llama-3.2-1B-Instruct

    • ダウンロード

      import transformers
      from transformers import (
        AutoTokenizer
      )
      
      import torch
      
      MODEL_URI  = "meta-llama/Llama-3.2-1B-Instruct"
      MODEL_PATH     = 'models/Llama-3.2-1B-Instruct'
      TOKENIZER_PATH = 'models/Llama-3.2-1B-Instruct'
      
      # model
      model = transformers.AutoModelForCausalLM.from_pretrained(
          MODEL_URI,
          torch_dtype=torch.bfloat16,
          trust_remote_code=True,
          force_download=True
        )
      model.save_pretrained(MODEL_PATH) 
      
      # tokenizer
      tokenizer = AutoTokenizer.from_pretrained(MODEL_URI)
      tokenizer.save_pretrained(TOKENIZER_PATH) 
      
    • ロード

      # model
      model = AutoModelForCausalLM.from_pretrained(MODEL_PATH)
      
      # tokenizer
      tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_PATH)
      if tokenizer.pad_token is None:
          tokenizer.pad_token = tokenizer.eos_token # コレは要るん?
      
    • print(model)の出力

      ...
      

Instruction Tuning

Instruction Tuningは、言語モデルに 命令(Instruction)とその応答(Response)を学習させることで、指示に従う能力を高める手法

変換関数

  • Instruction Tuningのプロンプト・フォーマット / テンプレートみたいな(実際に使う時はスペースを詰める)。

    • 日本語、指示、応答

      text = f"以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい\n\n### 指示:   \n{example['instruction'][i]}                                      \n\n### 応答: \n{example['output'][i]}<|endoftext|>"
      
    • 英語、question, context, answer
      ちなみに、コンテキストには、RAGのチャンクの様な参考情報が入るイメージ

      text = f"Please answer the question based on the given context.              \n\n### question\n{example['question'][i]}\n\n ### context\n{example['context'][i]}\n\n### answer\n{example['answer'][i]}<|endoftext|>"
      
  • SFTTrainerのformatting_func引数に渡すプロンプトフォーマット変換用関数を定義(末尾にeos_token文字列を追加)

    • kunishou/databricks-dolly-15k-ja

      print(tokenizer.eos_token)
      #'<|endoftext|>'
      
      def formatting_prompts_func(example):
          output_texts = []
          for i in range(len(example['instruction'])):
              text = f"以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい\n\n### 指示:\n{example['instruction'][i]}\n\n### 応答:\n{example['output'][i]}<|endoftext|>"
              output_texts.append(text)
          return output_texts
      
    • 自作JSON(jsonl)

      def formatting_prompts_func(example):
          output_texts = []
          for i in range(len(example['question'])):
              text = f"Please answer the question based on the given context.\n\n### question\n{example['question'][i]}\n\n### context\n{example['context'][i]}\n\n### answer\n{example['answer'][i]}<|endoftext|>"
              output_texts.append(text)
          return output_texts
      

損失計算

  • Instruction + Responseの両方を含むプロンプトを使って学習するが、損失計算は「全トークン」vs「応答部分のみ」がある。

    • 「すべてのトークンを損失計算対象にする(全体を正解として学習)」vs「応答部分だけを損失計算対象にする(Instructionは条件として与えるのみ)」
    • 後者の方が「モデルが指示を理解し、応答を生成する能力」に焦点を絞るため、効果的とされ、多くの先行研究でも応答部分のみを損失計算するのが一般的
  • SFTTrainerのdata_collator引数に渡すDataCollatorForCompletionOnlyLMを定義
    使用するにはpacking=Falseが必要で、この場合、dataset_text_field か formatting_funcを指定する必要あり。

    • template

      • response_templateは必須指定

        response_template = "### 応答:\n" # "### answer\n"
        
      • instruction_templateは複数回の対話形式の場合に必要(1問1答形式の場合は不要)

        instruction_template = "### 指示:\n" # "### question\n"
        
    • response_template以降のトークンだけを labels に設定
      (他は、PyTorch の CrossEntropyLoss で無視されるラベル = -100)

      from trl import DataCollatorForCompletionOnlyLM
      
      # response_templateは必須指定
      response_template = "### 応答:\n"
      collator = DataCollatorForCompletionOnlyLM(response_template, tokenizer=tokenizer)
      

SFTTrainerの構成

  • ポイント

    • formatting_funcで1問1答形式の文字列を作る。

    • data_collatorで応答部分だけを損失対象にする。

    • packing=Falseにより、packingせずに処理する。

      from transformers import TrainingArguments
      from trl import SFTTrainer
      
      # SFTTrainerはTrainingArgumentsを使用することができる。
      # 指定しない場合、TrainingArgumentsのデフォルトが指定される。
      args = TrainingArguments(
          output_dir='./output',
          num_train_epochs=2,
          gradient_accumulation_steps=8,
          per_device_train_batch_size=8,
          save_strategy="no",
          logging_steps=20,
          lr_scheduler_type="constant",
          save_total_limit=1,
          fp16=True,
      )
      
      trainer = SFTTrainer(
          model,
          args=args,
          train_dataset=dolly_train_dataset,
          formatting_func=formatting_prompts_func,
          max_seq_length=1024,
          data_collator=collator,
      )
      
  • 指定したデータセットがプロンプトフォーマットに変換されていることを確認

    print(trainer.train_dataset)
    # Dataset({
    #     features: ['input_ids', 'attention_mask'],
    #     num_rows: 10417
    # })
    
    print(tokenizer.decode(trainer.train_dataset[0]['input_ids']))
    # 以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい
    #
    # ### 指示:
    # XXXXXXXXXX
    #
    # ### 応答:
    # YYYYYYYYYY<|endoftext|>
    
  • 損失対象の確認(Instruction部分やpaddingには -100、応答部分にのみ数値が入る)

    from torch.utils.data import DataLoader
    loader = DataLoader(trainer.train_dataset, collate_fn=collator, batch_size=8)
    
    batch = next(iter(loader))
    print(batch['labels'][0])
    #tensor([ -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,   275, 19052,  4044,  2048,   431,   367,
    #            0,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,  -100,
    #         -100,  -100,  -100,  -100,  -100])
    

学習・保存

  • 学習・保存

    trainer.train()
    trainer.save_model()
    
  • 学習済みモデルのロード

    • cyberagent/open-calm-large

      lora_model = AutoModelForCausalLM.from_pretrained('./output', device_map="auto")
      

推論の実行

  • ロード

    • cyberagent/open-calm-large\

      model = AutoModelForCausalLM.from_pretrained(MODEL_PATH, device_map="auto", torch_dtype=torch.float16)
      tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
      
  • プロンプトのトークン化(pt = torch.Tensor)

    inputs = tokenizer('...プロンプト...', return_tensors="pt").to(model.device)
    print(inputs) # テンソルのディクショナリになっている。
    
  • 推論時、勾配計算しないブロックに格納、
    また、**inputsで「ディクショナリのキー・値」が「キーワード引数・引数」に展開。

    with torch.no_grad():
        tokens = model.generate(
            **inputs,
            max_new_tokens=64,
            do_sample=True,
            temperature=0.7,
            top_p=0.9,
            repetition_penalty=1.05,
            pad_token_id=tokenizer.pad_token_id,
        )
    
  • デトークン化した推論結果の表示

    output = tokenizer.decode(tokens[0], skip_special_tokens=True)
    print(output)
    

LoRAによるチューニング

コードの差異

  • peftでLoraConfigを定義して、SFTTrainerのpeft_config引数に指定
    (data_collator引数やformatting_func引数などは前述と同様のものを指定し。)

    from peft import LoraConfig
    
    peft_config = LoraConfig(
        r=8,
        lora_alpha=16,
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM",
    )
    
    args = TrainingArguments(
        output_dir='./output_lora',
        ...同上...,
    )
    
    trainer = SFTTrainer(
        ...同上...,
        peft_config=peft_config,
    )
    
    trainer.train()
    trainer.save_model()
    
  • モデルのロード

    • cyberagent/open-calm-large
      Adapter + ベース・パラメタ = adapter_config.json の base_model_name_or_path

      lora_model = AutoModelForCausalLM.from_pretrained('./output_lora', device_map="auto")
      

確認事項

  • LoRA Adapterが挿入されない層のパラメタ
    SFTTrainerにmodelを渡す前、渡した前、学習後で変化なしのハズ。

    • cyberagent/open-calm-large

      # 最初の層のFFNの一部を確認する
      # この時点ではrequires_grad=Trueになっている
      first_ffn_param = model.gpt_neox.layers[0].mlp.dense_h_to_4h.weight
      print(first_ffn_param)
      #Parameter containing:
      #tensor([[-0.1072,  0.0417, -0.0432,  ..., -0.0873, -0.1708, -0.1608],
      #        [-0.0934,  0.0773,  0.0074,  ..., -0.2107,  0.0881, -0.0803],
      #        [-0.0506, -0.1282, -0.1511,  ...,  0.1120, -0.0126, -0.1172],
      #        ...,
      #        [ 0.1274, -0.0688,  0.1787,  ...,  0.1432,  0.0266, -0.1370],
      #        [-0.1108, -0.0758,  0.0035,  ..., -0.0404, -0.1801,  0.0338],
      #        [ 0.0669,  0.0399, -0.0443,  ..., -0.2275, -0.1323,  0.0034]],
      #       device='cuda:0', requires_grad=True)
      
    • Llama-3.2-1B-Instruct

  • SFTTrainerにmodelを渡した後でSFTTrainerからmodelにアクセス
    LoraConfigのtarget_modulesに指定しなくても、いい感じにAdapterが挿入される。

    • cyberagent/open-calm-large
      「(attention): GPTNeoXAttention」の「(query_key_value): Linear」に「lora_dropout、lora_A、lora_B、lora_embedding_A / lora_embedding_B」が追加

      PeftModelForCausalLM(
        (base_model): LoraModel(
          (model): GPTNeoXForCausalLM(
            (gpt_neox): GPTNeoXModel(
              (embed_in): Embedding(52096, 1536)
              (emb_dropout): Dropout(p=0.0, inplace=False)
              (layers): ModuleList(
                (0-23): 24 x GPTNeoXLayer(
                  (input_layernorm): LayerNorm((1536,), eps=1e-05, elementwise_affine=True)
                  (post_attention_layernorm): LayerNorm((1536,), eps=1e-05, elementwise_affine=True)
                  (post_attention_dropout): Dropout(p=0.0, inplace=False)
                  (post_mlp_dropout): Dropout(p=0.0, inplace=False)
                  (attention): GPTNeoXAttention(
                    (rotary_emb): GPTNeoXRotaryEmbedding()
                    (query_key_value): Linear(
                      in_features=1536, out_features=4608, bias=True
                      (lora_dropout): ModuleDict(
                        (default): Dropout(p=0.05, inplace=False)
                      )
                      (lora_A): ModuleDict(
                        (default): Linear(in_features=1536, out_features=8, bias=False)
                      )
                      (lora_B): ModuleDict(
                        (default): Linear(in_features=8, out_features=4608, bias=False)
                      )
                      (lora_embedding_A): ParameterDict()
                      (lora_embedding_B): ParameterDict()
                    )
                    (dense): Linear(in_features=1536, out_features=1536, bias=True)
                    (attention_dropout): Dropout(p=0.0, inplace=False)
                  )
                  (mlp): GPTNeoXMLP(
                    (dense_h_to_4h): Linear(in_features=1536, out_features=6144, bias=True)
                    (dense_4h_to_h): Linear(in_features=6144, out_features=1536, bias=True)
                    (act): GELUActivation()
                  )
                )
              )
              (final_layer_norm): LayerNorm((1536,), eps=1e-05, elementwise_affine=True)
            )
            (embed_out): Linear(in_features=1536, out_features=52096, bias=False)
          )
        )
      )
      
    • Llama-3.2-1B-Instruct

  • 学習されるパラメタ数を確認
    LoRAにより、trainableなパラメタは、かなり少なくなっているはず。

    • cyberagent/open-calm-large

      trainer.model.print_trainable_parameters()
      #trainable params: 1,179,648 || all params: 841,178,112 || trainable%: 0.14023760047622352
      
    • Llama-3.2-1B-Instruct

  • SFTTrainerで学習した後のLoRA Adapterが挿入されない層のパラメタ

    • cyberagent/open-calm-large

      # Adapterが挿入されていない層はパラメタはbaseモデルのときとまったく同じ
      (lora_model.gpt_neox.layers[0].mlp.dense_h_to_4h.weight == first_ffn_param).all()
      #tensor(True, device='cuda:0') ## GPU上にあるテンソルの全要素が一致
      
    • Llama-3.2-1B-Instruct

NEFTuneによる学習

packingを用いた学習

学習結果の比較

参考

https://github.com/OpenTouryoProject/DxCommon/blob/master/Notebook/path/LLM_Fine-tuning2.ipynb

Qiita

Hugging Face

The AI community building the future.

github.com

https://github.com/huggingface/trl/blob/main/trl/

移行メモ

  • 「戻る」の並びにある自ページは、その旨を補ってプレーン・テキストとした。
  • 元 Wiki で見出しそのものが他ページ・同ページ内へのリンクになっていた箇所は、 GitHub Wiki では見出しからアンカが生成されるため、 見出しをプレーン・テキストとし、リンクは直下の本文に置いた。
  • PukiWiki のページ内アンカ(#xxxxxxxx)は GitHub Wiki では再現できないため、 同一ページ内のアンカは見出しから生成されるアンカに張り替え、 他ページのアンカを指すリンクは「〜(ページ名 の該当節を参照)」の形に置き換えた。
  • 元 Wiki の行頭空白によるプレフォーマット・ブロックは、 フェンス付きコードブロックにした。
  • PukiWiki の表は GitHub の表に変換した。

Tags: 移行, Hugging Face, trl, SFTTrainer, ファインチューニング, LLM, LoRA

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally