Llama-2が登場!8bit+LoRAでRLHFファインチューニングを試す方法はこちら

【Llama-2】8bit+LoRAでRLHFファインチューニングを試す【学習できることは確認】

Llama-2が出たのでRLHFを試してみました。

事前学習モデルでは教師ありファインチューニングをしてから行う必要がありますが、すでに調整されているモデルが公開されているのでそちらを使います。

前提

・モデルの申請などがすでに終わっていてモデルを使うことができる

・accelerateの設定(accelerate config)が終わっている

この記事で分かること

・Llama-2のRLHFファインチューニングの方法

・パラメータの変更方法

目次

学習モデル

meta-llama/Llama-2-7b-chat-hfを使います。

とりあえず一番小さい(これでも大きい)モデルでお試しです。

マシンパワーさえあれば70bとかもいけると思います。

あわせて読みたい
meta-llama/Llama-2-7b-chat-hf · Hugging Face We’re on a journey to advance and democratize artificial intelligence through open source and open science.

データセット

以下のデータセットのinstraction部分を入力として使っています。

ありがとうございます。

あわせて読みたい
kunishou/hh-rlhf-49k-ja · Datasets at Hugging Face We’re on a journey to advance and democratize artificial intelligence through open source and open science.

報酬モデル

報酬モデルについても自分で作るのは大変です。すでにあるものを使います。(とりあえず使うだけなので他のreward modelでも大丈夫です。)

あわせて読みたい
OpenAssistant/reward-model-deberta-v3-base · Hugging Face We’re on a journey to advance and democratize artificial intelligence through open source and open science.

日本語応答に対して適切に報酬を与えられるかわからないので、ここは他のモデルを選択する方がいい結果が得られると思います。

一応、OpenAssistantが公開している報酬モデルについては調査してみました。

あわせて読みたい
RLHFの報酬モデル分析:OpenAssistantの報酬モデルが日本語応答で機能するか調査してみた 言語モデルにおける強化学習では、重要な要素の一つに報酬関数の設計があります。 この報酬関数は、生成されたテキストがどれほど「良い」かを評価する役割を果たします...

環境

  • RTX3090

ライブラリ

pip install transformers
pip install accelerate
pip install peft
pip install trl
pip install datasets
pip install tqdm
pip install wandb # 必要な場合

コード全体

以下のコードをtrain.pyにコピペしてください。

学習はできますが、このままではしっかりとした学習ができるわけではないので調整が必要です。

また、トークナイザーなど(特殊トークンの関係とか)で間違えている場合もあるのでご了承ください。

# coding=utf-8
# Copyright 2023 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

import torch
from dataclasses import dataclass, field
from typing import Optional
from accelerate import Accelerator
from datasets import load_dataset
from peft import LoraConfig
from tqdm import tqdm
from transformers import (
    Adafactor,
    AutoTokenizer,
    HfArgumentParser,
    pipeline
)

from trl import AutoModelForCausalLMWithValueHead, PPOConfig, PPOTrainer, set_seed
from trl.core import LengthSampler

tqdm.pandas()


@dataclass
class ScriptArguments:
    """
    デフォルト値を変更するだけでいいかも
    """
    model_name: Optional[str] = field(default="meta-llama/Llama-2-7b-chat-hf", metadata={"help": "the model name"})
    tokenizer_name: Optional[str] = field(default="meta-llama/Llama-2-7b-chat-hf", metadata={"help": "the tokenizer name"})
    reward_model_name: Optional[str] = field(default="OpenAssistant/reward-model-deberta-v3-base", metadata={"help": "the reward model name"})
    dataset_name: Optional[str] = field(default="kunishou/hh-rlhf-49k-ja", metadata={"help": "the dataset name"})
    log_with: Optional[str] = field(default="wandb", metadata={"help": "use 'wandb' to log with wandb"})
    learning_rate: Optional[float] = field(default=1.41e-5, metadata={"help": "the learning rate"})
    max_length: Optional[int] = field(default=512, metadata={"help": "maximum length for input"})
    output_max_length: Optional[int] = field(default=128, metadata={"help": "maximum length for generation"})
    mini_batch_size: Optional[int] = field(default=1, metadata={"help": "the PPO minibatch size"})
    batch_size: Optional[int] = field(default=32, metadata={"help": "the batch size"})
    ppo_epochs: Optional[int] = field(default=4, metadata={"help": "the number of ppo epochs"})
    gradient_accumulation_steps: Optional[int] = field(
        default=4, metadata={"help": "the number of gradient accumulation steps"}
    )
    adafactor: Optional[bool] = field(default=False, metadata={"help": "whether to use the adafactor optimizer"})
    early_stopping: Optional[bool] = field(default=False, metadata={"help": "whether to early stop"})
    target_kl: Optional[float] = field(default=0.1, metadata={"help": "kl target for early stopping"})
    reward_baseline: Optional[float] = field(
        default=0.0,
        metadata={"help": "a baseline value that is subtracted from the reward"},
    )
    batched_gen: Optional[bool] = field(default=False, metadata={"help": "whether to use the batched text gen"})
    save_freq: Optional[int] = field(default=None, metadata={"help": "n steps to save the model"})
    output_dir: Optional[str] = field(default="./checkpoints/tuning_llama2_rl/",
                                      metadata={"help": "n steps to save the model"})
    seed: Optional[int] = field(default=0, metadata={"help": "the seed"})
    
    
def main():
    parser = HfArgumentParser(ScriptArguments)
    script_args: ScriptArguments = parser.parse_args_into_dataclasses()[0]

    set_seed(script_args.seed)

    # ここでデータセットを変換する
    def build_dataset(
            tokenizer, dataset_name
    ):

        train_dataset = load_dataset(dataset_name, split="train")
        original_columns = train_dataset.column_names
        num_proc = 24

        def preprocess_function(examples):
            new_examples = {
                "query": [],
                "input_ids": [],
            }
            for question in examples["instruction"]:
                query = "Question: " + question + "\n\nAnswer: "
                tokenized_question = tokenizer(query, truncation=True)
                new_examples["query"].append(query)
                new_examples["input_ids"].append(tokenized_question["input_ids"])

            return new_examples

        ds = train_dataset.map(
            preprocess_function,
            batched=True,
            num_proc=num_proc,
            remove_columns=original_columns,
        )
        ds = ds.filter(lambda x: len(x["input_ids"]) < script_args.max_length, batched=False)

        ds.set_format(type="torch")
        return ds


    def collator(data):
        return dict((key, [d[key] for d in data]) for key in data[0])

    reward_model_name = script_args.reward_model_name
    config = PPOConfig(
        model_name=script_args.model_name,
        learning_rate=script_args.learning_rate,
        log_with=script_args.log_with,
        batch_size=script_args.batch_size,
        mini_batch_size=script_args.mini_batch_size,
        gradient_accumulation_steps=script_args.gradient_accumulation_steps,
        optimize_cuda_cache=True,
        early_stopping=script_args.early_stopping,
        target_kl=script_args.target_kl,
        ppo_epochs=script_args.ppo_epochs,
        seed=script_args.seed,
    )

    rw_kwargs = {
        "return_all_scores": True,
        "function_to_apply": "none",
        "batch_size": 16,
        "truncation": True
    }

    tokenizer = AutoTokenizer.from_pretrained(script_args.model_name)
    if getattr(tokenizer, "pad_token", None) is None:
        tokenizer.pad_token = tokenizer.eos_token

    dataset = build_dataset(tokenizer, script_args.dataset_name)

    current_device = Accelerator().local_process_index

    lora_config = LoraConfig(
        r=16,
        lora_alpha=32,
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM",
    )
    model = AutoModelForCausalLMWithValueHead.from_pretrained(
        config.model_name,
        load_in_8bit=True,
        device_map={"": current_device},
        peft_config=lora_config,
    )

    optimizer = None
    if script_args.adafactor:
        optimizer = Adafactor(
            filter(lambda p: p.requires_grad, model.parameters()),
            scale_parameter=False,
            relative_step=False,
            warmup_init=False,
            lr=config.learning_rate,
        )

    ppo_trainer = PPOTrainer(
        config,
        model,
        ref_model=None,
        tokenizer=tokenizer,
        dataset=dataset,
        data_collator=collator,
        optimizer=optimizer,
    )

    device = ppo_trainer.accelerator.device
    if ppo_trainer.accelerator.num_processes == 1:
        device = 0 if torch.cuda.is_available() else "cpu"

    reward_tokenizer = AutoTokenizer.from_pretrained(reward_model_name)
    reward_model = pipeline(
        "text-classification",
        model=reward_model_name,
        device_map={"": current_device},
        model_kwargs={"load_in_8bit": True},
        tokenizer=reward_tokenizer,
    )

    generation_kwargs = {
        # "min_length": -1,
        "top_k": 0.0,
        "top_p": 1.0,
        "do_sample": True,
        "pad_token_id": tokenizer.eos_token_id,
    }
    output_min_length = 32
    output_max_length = script_args.output_max_length
    output_length_sampler = LengthSampler(output_min_length, output_max_length)

    for epoch, batch in tqdm(enumerate(ppo_trainer.dataloader)):
        question_tensors = batch["input_ids"]

        response_tensors = ppo_trainer.generate(
            question_tensors,
            return_prompt=False,
            length_sampler=output_length_sampler,
            **generation_kwargs,
        )
        batch["response"] = tokenizer.batch_decode(response_tensors, skip_special_tokens=True)

        texts = [q + r for q, r in zip(batch["query"], batch["response"])]
        reward_outputs = reward_model(texts, **rw_kwargs)
        rewards = [torch.tensor(output[0]["score"] - script_args.reward_baseline) for output in reward_outputs]

        stats = ppo_trainer.step(question_tensors, response_tensors, rewards)
        ppo_trainer.log_stats(stats, batch, rewards)

        if script_args.save_freq and epoch and epoch % script_args.save_freq == 0:
            ppo_trainer.save_pretrained(script_args.output_dir + f"step_{epoch}")

    ppo_trainer.save_pretrained(script_args.output_dir + f"step_{epoch}")

if __name__ == "__main__":
    main()

accelerateの設定が済んでいて、すぐ実行したい方は以下を実行してください。

accelerate launch train.py

コード解説

ScriptArguments

以下のように使ってください。

accelerate launch --multi_gpu --num_machines 1 \
    train.py \
    --log_with wandb \
    --save_freq 10 \
スクロールできます
引数名デフォルト値役割
model_name“meta-llama/Llama-2-7b-chat-hf”使用するCasual LMモデルの名前
tokenizer_name“meta-llama/Llama-2-7b-chat-hf”使用するトークナイザーの名前
reward_model_name“OpenAssistant/reward-model-deberta-v3-base”使用する報酬モデルの名前
dataset_name“kunishou/hh-rlhf-49k-ja”使用するデータセットの名前
log_with“wandb”ロギングに使用するツール(’wandb’であればWeights & Biases)
learning_rate1.41e-5学習率
max_length512入力の最大長
output_max_length128生成するテキストの最大長
mini_batch_size1PPOのミニバッチサイズ
batch_size32バッチサイズ
ppo_epochs4PPOのエポック数
gradient_accumulation_steps4勾配累積ステップ数
adafactorFalseAdafactorオプティマイザーの使用有無
early_stoppingFalse早期停止の使用有無
target_kl0.1早期停止のためのKLダイバージェンスの目標値
reward_baseline0.0報酬から差し引かれるベースライン値
batched_genFalseバッチ単位でのテキスト生成の使用有無
save_freqNoneモデルを保存するステップ数
output_dir“./checkpoints/tuning_llama_rl/”モデルを保存するディレクトリ
seed0乱数のシード

build_dataset

build_datasetでデータセットの形を整えています。

また、以下の部分で入力文章を調整できます。

        def preprocess_function(examples):
            new_examples = {
                "query": [],
                "input_ids": [],
            }
            for question in examples["instruction"]:
                query = "Question: " + question + "\n\nAnswer: "
                tokenized_question = tokenizer(query, truncation=True)
                new_examples["query"].append(query)
                new_examples["input_ids"].append(tokenized_question["input_ids"])

            return new_examples

下記のような感じになります。

Question:あなたの名前は?

Answer:

ここはHuman:~Assistant:のようにしても大丈夫です。

結果

wandbを使用すると学習状況が簡単に分かります。

他にも色々とみることができます。

感想

やっぱり難しいなという印象です。

報酬モデル自体の調整も必要で、今回使った報酬モデルは私の評価とあまり合致していないと感じました。

一方で、とりあえずは入力文だけ用意すればいいので、労力は教師ありファインチューニングよりは少なそうだと思ったので色々試していきます。

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!

コメント

コメントする

目次