「[[.NET 開発基盤部会 Wiki>http://dotnetdevelopmentinfrastructure.osscons.jp]]」は、「[[Open棟梁Project>https://github.com/OpenTouryoProject/]]」,「[[OSSコンソーシアム .NET開発基盤部会>https://www.osscons.jp/dotNetDevelopmentInfrastructure/]]」によって運営されています。 -[[戻る>テキスト生成系(Transformer系)#j2f01d54]] > [[チューニング、拡張系>テキスト生成系(Transformer系)#j2f01d54]] --[[LLMのPE]] --[[LLMのRAG]] --[[LLMのFT]] ---[[huggingface/transformers Trainer]] ---huggingface/trl SFTTrainer --[[LLMエージェント]] *目次 [#ndebe625] #contents *概要 [#tfd2a307] huggingface/trl SFTTrainer **huggingface/trl [#f564f186] -Hugging Faceが提供するtransformersの拡張 -Transformer Reinforcement Learningの略で[[指示・強化学習>LLMのFT#nc47f676]]の機能を提供する。 **SFTTrainer [#a5f88e61] ***trlに含まれるトレーナー・クラス [#vf315bc4] -教師ありファインチューニング(SFT)に特化したトレーナー。 -Trainerクラスを継承していて、SFT向けの機能を追加している。 ***SFTTrainerでできること [#h31da3eb] -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による学習 *詳細 [#ud2fe008] **実装準備 [#cd8f6842] ***インストール [#ddf11f78] -領域が足りないことがあるので注意 --「pip cache purge」&「TMPDIR=/mnt/tmp pip install」を使用 --必要に応じて[[ディスク追加>OSSのLLM#u0d612e8]]を行う。 -ModuleNotFoundError: No module named '_lzma' ***[[プロキシ環境>Python#y9ac7218]] [#w16e12cf] ***データセット [#vb74629b] -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)の値が空のデータに絞っている。 ***モデル [#b6baa2c4] -モデルとトークナイザ(文字列と数値IDを相互変換する道具) -2つの例を見ると、オプションに違いがあるが、コレは? |オプション|説明|h |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 [#u28e7e53] Instruction Tuningは、言語モデルに 命令(Instruction)とその応答(Response)を学習させることで、指示に従う能力を高める手法 ***変換関数 [#l190987d] -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 ***損失計算 [#tfe911c1] -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の構成 [#t3a58389] -ポイント --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]) ***学習・保存 [#s8e8326a] -学習・保存 trainer.train() trainer.save_model() -学習済みモデルのロード --cyberagent/open-calm-large lora_model = AutoModelForCausalLM.from_pretrained('./output', device_map="auto") ***推論の実行 [#k0be0ea6] -ロード --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によるチューニング [#zb2bd049] ***コードの差異 [#k62a4fb0] -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") ***確認事項 [#k7d159ce] -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による学習 [#m6d89a97] **packingを用いた学習 [#c1f0b2f2] **学習結果の比較 [#qe8b9bad] *参考 [#w465ae3e] https://github.com/OpenTouryoProject/DxCommon/blob/master/Notebook/path/LLM_Fine-tuning2.ipynb **Qiita [#x7ffee2d] -huggingface/TRLのSFTTrainerクラスを使えばLLMのInstruction Tuningのコードがスッキリ書けてとても便利です~ https://qiita.com/m__k/items/23ced0db6846e97d41cd -OpenCALM-7Bのコードリーディング(基本編)~ https://zenn.dev/hijikix/articles/549180d467df3c **Hugging Face [#n4339d0b] The AI community building the future. -TRL - Transformer Reinforcement Learning~ https://huggingface.co/docs/trl/index -Supervised Fine-tuning Trainer~ https://huggingface.co/docs/trl/sft_trainer **github.com [#aee3b3d2] https://github.com/huggingface/trl/blob/main/trl/ -https://github.com/huggingface/trl/blob/main/trl/scripts/sft.py