メインコンテンツへスキップ
見出し画像

PEFTをWindowsから遊ぶメモ

    このメモを読むと

    ・PEFTを導入できる
    ・ローカルLLMをファインチューニングできる

    検証環境

    ・Windows11
    ・VRAM24GB
    ・ローカル(Anaconda)
    ・2023/6/M時点

    事前準備

    Anacondaを使うメモ|おれっち (note.com)
    Gitを使うメモ|おれっち (note.com)

    PEFTとは

    ローカルLLMのファインチューニングを手軽に実現できるライブラリ。
    実装してLoRAでファインチューニングしてみます。

    PEFT導入

    とても簡単です!

    1. 仮想環境を作成し、環境切替

    conda create -n PEFT python=3.10
    activate PEFT

     2. 追加パッケージのインストール

    pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
    pip install datasets accelerate loralib sentencepiece transformers
    python -m pip install https://github.com/jllllll/bitsandbytes-windows-webui/raw/main/bitsandbytes-0.39.0-py3-none-any.whl
    pip install git+https://github.com/huggingface/peft.git

    完了です!

    PEFTを使ってみる

    PEFTからLoRAでファインチューニングします。
    用意するものは以下の二つです。
     ・ベースモデル
     ・データセット(学習データ)

    それぞれこちらをお借りしました。
    ベースモデル:rinna/japanese-gpt-neox-3.6b-instruction-ppo
    データセット:bbz662bbz/databricks-dolly-15k-ja-gozaru

    では実際に学習とテストをしてみましょう。

    学習

    好きな名前で下記スクリプトを作成し実行します。

    from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_int8_training
    from transformers import AutoTokenizer, AutoModelForCausalLM, Trainer, TrainingArguments, DataCollatorForLanguageModeling
    from datasets import load_dataset
    
    # 基本パラメータ
    base_model = "rinna/japanese-gpt-neox-3.6b-instruction-ppo"
    dataset = "bbz662bbz/databricks-dolly-15k-ja-gozaru"
    peft_name = "test-rinna-3.6b"
    output_dir = "test-rinna-3.6b-results"
    
    # トレーニング用パラメータ
    eval_steps = 100
    save_steps = 200
    logging_steps = 20
    epochs = 3
    max_steps = 250 # 0にするとepochsに応じて自動設定
    
    # LoRA用パラメータ
    lora_r = 8
    lora_alpha = 16
    lora_dropout = 0.1
    
    # 他パラメータ
    VAL_SET_SIZE = 0.2 # 検証分割比率
    CUTOFF_LEN = 512  # コンテキスト長の上限
    
    # ベースモデルの読み込み
    model = AutoModelForCausalLM.from_pretrained(
        base_model, 
        device_map='auto',
        load_in_8bit=True,
    )
    tokenizer = AutoTokenizer.from_pretrained(base_model, use_fast=False) # Rinnaのトークナイザーでは、「use_fast=False」も必要になる
    
    # PEFT(LoRA)の設定
    config = LoraConfig(r=lora_r,
                        lora_alpha=lora_alpha,
                        lora_dropout=lora_dropout,
                        inference_mode=False,
                        task_type=TaskType.CAUSAL_LM,
    )
    model = prepare_model_for_int8_training(model)
    model = get_peft_model(model, config)
    model.enable_input_require_grads()  
    model.gradient_checkpointing_enable()   
    model.print_trainable_parameters()  # 学習可能パラメータの確認
    
    # トークナイズ関数
    def tokenize(prompt, tokenizer):
        result = tokenizer(prompt,
                           truncation=True,
                           max_length=CUTOFF_LEN,
                           padding=False,
        )
        return {"input_ids": result["input_ids"],
                "attention_mask": result["attention_mask"],
        }
    
    # データセットの準備
    data = load_dataset(dataset)
    
    # プロンプトテンプレートの準備
    # 回答の最後にベースモデル特有のEOSトークン</s>を挿入
    def generate_prompt(data_point):
        if data_point["input"]:
            result = f"""ユーザー:{data_point["instruction"]}
    入力:{data_point["input"]}
    システム:{data_point["output"]}</s>"""
        else:
            result = f"""ユーザー:{data_point["instruction"]}
    システム:{data_point["output"]}</s>"""
    
        result = result.replace('\n', '<NL>')   # 改行→<NL>
        return result
    
    # 学習データと検証データの準備
    train_val = data["train"].train_test_split(test_size=VAL_SET_SIZE, shuffle=True, seed=42)
    train_data = train_val["train"]
    val_data = train_val["test"]
    train_data = train_data.shuffle().map(lambda x: tokenize(generate_prompt(x), tokenizer))
    val_data = val_data.shuffle().map(lambda x: tokenize(generate_prompt(x), tokenizer))
    
    # トレーナーの定義
    trainer = Trainer(
        model = model, 
        train_dataset = train_data,
        eval_dataset = val_data,
        args = TrainingArguments(
            num_train_epochs=epochs,
            learning_rate=3e-4,
            logging_steps=logging_steps,
            evaluation_strategy="steps",
            save_strategy="steps",
            max_steps=max_steps,
            eval_steps=eval_steps,
            save_steps=save_steps,
            output_dir=output_dir,
            report_to="none",
            save_total_limit=3,
            push_to_hub=False,
            auto_find_batch_size=True
        ),
        data_collator= DataCollatorForLanguageModeling(tokenizer, mlm=False),
    )
    
    model.config.use_cache = False  # 警告を黙らせます
    trainer.train()
    model.config.use_cache = True   # 推論のために再度有効化
    trainer.model.save_pretrained(peft_name)    # LoRAモデルの保存
    print("Done!")

    お試しなので訓練ステップ"max_steps"を250に設定しています。
    0にすると本来の回数?に自動設定されます。

    画像
    Done! が出れば学習完了

    VRAM使用量は10GB前後でした。

    推論

    学習結果をテストします。
    好きな名前で下記スクリプトを作成し実行します。

    import time
    from peft import PeftModel
    from transformers import AutoModelForCausalLM, AutoTokenizer
    
    # 基本パラメータ
    base_model = "rinna/japanese-gpt-neox-3.6b-instruction-ppo"
    peft_name = "test-rinna-3.6b"
    
    # 入力文章
    input_text = "カレーにジャガイモは入れるべき?"
    
    # ベースモデルの読み込み
    model = AutoModelForCausalLM.from_pretrained(
        base_model, 
        device_map='auto',
        load_in_8bit=True,
    )
    tokenizer = AutoTokenizer.from_pretrained(base_model, use_fast=False) # Rinnaのトークナイザーでは、「use_fast=False」も必要になる
    
    # PEFT(LoRA)の読み込み
    model = PeftModel.from_pretrained(model, peft_name)
    model.eval()# 評価モード
    
    # プロンプトテンプレートの準備
    def generate_prompt(data_point):
        result = f"""ユーザー:{data_point["instruction"]}
    システム:"""
    
        result = result.replace('\n', '<NL>')   # 改行→<NL>
        return result
    
    # テキスト生成関数の定義
    def textgen(instruction,input=None,maxTokens=256):
        prompt = generate_prompt({'instruction':instruction,'input':input})
        input_ids = tokenizer(prompt, 
                              return_tensors="pt", 
                              truncation=True, 
                              add_special_tokens=False).input_ids.cuda()
        outputs = model.generate(input_ids=input_ids, 
                                 max_new_tokens=maxTokens, 
                                 do_sample=True,
                                 temperature=0.7, 
                                 top_p=0.75, 
                                 top_k=40,         
                                 no_repeat_ngram_size=2,
        )
        outputs = outputs[0].tolist()
        #print(tokenizer.decode(outputs))
    
        # EOSトークンにヒットしたらデコード完了
        if tokenizer.eos_token_id in outputs:
            eos_index = outputs.index(tokenizer.eos_token_id)
            decoded = tokenizer.decode(outputs[:eos_index])
    
            # レスポンス内容のみ抽出
            sentinel = "システム:"
            sentinelLoc = decoded.find(sentinel)
            if sentinelLoc >= 0:
                result = decoded[sentinelLoc+len(sentinel):]
                return(result.replace("<NL>", "\n"))  # <NL>→改行
            else:
                return('Warning: Expected prompt template to be emitted.  Ignoring output.')
        else:
            return('Warning: no <eos> detected ignoring output')
    
    start_time = time.time()
    print(f"Q: {input_text}\nA: {textgen(input_text)}")
    end_time = time.time()
    elapsed_time = end_time - start_time
    print(f"Elapsed time: {elapsed_time} seconds")

    Q: カレーにジャガイモは入れるべき?
    A: ジャガイモとカレーは相性がいいですでござる。
    Elapsed time: 2.808380126953125 seconds

    出力

    VRAM使用量は6GB前後でした。

    おわり

    自分のtwitterをデータセットとして使用する方法も。試したい!

    参考資料

    ・PEFTを使用したLoRA (Windows10(WSL2不使用) bitsandbytes==0.37.0使用) - Qiita
    ・Google Colab で Rinna-3.6B のLoRAファインチューニングを試す|npaka (note.com)
    ・rinna-3bを試してみる (zenn.dev)


     
     
    あとから再構築できるようにメモ。 ビギナーなのでChatGPTに助けてもらってます。

    あなたへのおすすめ