見出し画像

Rinna-3.6BをAlpaca_Cleaned_Japaneseでファインチューニング

Rinna-3.6BをAlpaca_Cleaned_Japaneseでファインチューニングした
まずは結果から

{'train_runtime': 60495.5691, 'train_samples_per_second': 2.465, 'train_steps_per_second': 0.308, 'train_loss': 1.3123264420668668, 'epoch': 3.0}
### 指示:<NL>自然言語処理とは?<NL><NL>### 回答:<NL>Natural Language Processing(NLP)は、自然な言語理解と自然文の理解を可能にするコンピュータサイエンスの一分野です。NLUは人間の言語を理解し、コンピュータが言語の意味を解釈し解釈できるようにする技術ですが、NLIBはコンピュータ・ビジョンや音声認識などの他の技術と組み合わせて使用されます。</NLA></s>

Natural Language Processing(NLP)は、自然な言語理解と自然文の理解を可能にするコンピュータサイエンスの一分野です。NLUは人間の言語を理解し、コンピュータが言語の意味を解釈し解釈できるようにする技術ですが、NLIBはコンピュータ・ビジョンや音声認識などの他の技術と組み合わせて使用されます。</NLA>
### 指示:<NL>日本の首都は?<NL><NL>### 回答:<NL>東京は日本で最大の都市であり、人口300,000,000人以上を抱えています。< NL name>Japan</ N L></s>

東京は日本で最大の都市であり、人口300,000,000人以上を抱えています。< NL name>Japan</ N L>
### 指示:<NL>富士山の登山ルートはいくつある?<NL><NL>### 回答:<NL>日本最高峰の富士山は、静岡県側と山梨県側、2つのルートがあります。静岡県側のルートには、須走口、吉田口(吉田ルート)、御殿場口の3つの登山道がありますが、山梨県側のコースには須山口、河口湖口があります(富士宮口は閉鎖されています)。</Nli></s>

日本最高峰の富士山は、静岡県側と山梨県側、2つのルートがあります。静岡県側のルートには、須走口、吉田口(吉田ルート)、御殿場口の3つの登山道がありますが、山梨県側のコースには須山口、河口湖口があります(富士宮口は閉鎖されています)。</Nli>
### 指示:<NL>光の三原色とは?<NL><NL>### 回答:<NL>赤、緑、青の3色は、光の3原色と呼ばれています。赤はRed、Green、Blueの光の波のスペクトルで、色を表現します。緑はGreenで青はBlueです。</Nov></s>

赤、緑、青の3色は、光の3原色と呼ばれています。赤はRed、Green、Blueの光の波のスペクトルで、色を表現します。緑はGreenで青はBlueです。</Nov>
### 指示:<NL>エヴァンゲリオンの登場人物で最も謎が多いのは?<NL><NL>### 回答:<NL>謎の多いキャラクターは、碇シンジです。彼は、謎の多いキャラクターであり、謎に包まれたキャラクターですが、彼の過去と動機を理解することは困難です< NL 。<NA></s>

謎の多いキャラクターは、碇シンジです。彼は、謎の多いキャラクターであり、謎に包まれたキャラクターですが、彼の過去と動機を理解することは困難です< NL 。<NA>

いい感じである。
以下、再現方法

まず、npaka先生のファインチューニングを参考にソースコードの形にした。

# 基本パラメータ
model_name = "rinna/japanese-gpt-neox-3.6b"
dataset = "shi3z/alpaca_cleaned_ja_json"
peft_name = "lora-rinna-3.6b"
output_dir = "lora-rinna-3.6b-results-e6"
from transformers import AutoTokenizer

# トークナイザーの準備
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
# 基本パラメータ
model_name = "rinna/japanese-gpt-neox-3.6b"
dataset = "shi3z/alpaca_cleaned_ja_json"
peft_name = "lora-rinna-3.6b"
output_dir = "lora-rinna-3.6b-results-e6"
from transformers import AutoTokenizer

# トークナイザーの準備
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)
# スペシャルトークンの確認
print(tokenizer.special_tokens_map)
print("bos_token :", tokenizer.eos_token, ",", tokenizer.bos_token_id)
print("eos_token :", tokenizer.bos_token, ",", tokenizer.eos_token_id)
print("unk_token :", tokenizer.unk_token, ",", tokenizer.unk_token_id)
print("pad_token :", tokenizer.pad_token, ",", tokenizer.pad_token_id)
CUTOFF_LEN = 256  # コンテキスト長


# トークナイズ
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"],
    }
# トークナイズの動作確認
tokenize("hi there", tokenizer)
from datasets import load_dataset

# データセットの準備
data = load_dataset(dataset)
# プロンプトテンプレートの準備
def generate_prompt(data_point):
    if data_point["input"]:
        result = f"""### 指示:
{data_point["instruction"]}

### 入力:
{data_point["input"]}

### 回答:
{data_point["output"]}"""
    else:
        result = f"""### 指示:
{data_point["instruction"]}

### 回答:
{data_point["output"]}"""

    # 改行→<NL>
    result = result.replace('\n', '<NL>')
    return result

# プロンプトテンプレートの確認
print(generate_prompt(data["train"][5]))
VAL_SET_SIZE = 2000

# 学習データと検証データの準備
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))
from transformers import AutoModelForCausalLM

# モデルの準備
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    #load_in_8bit=True,
    device_map="auto",
)
from peft import LoraConfig, get_peft_model, prepare_model_for_int8_training, TaskType

# LoRAのパラメータ
lora_config = LoraConfig(
    r= 8, 
    lora_alpha=16,
    target_modules=["query_key_value"],
    lora_dropout=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM
)

# モデルの前処理
model = prepare_model_for_int8_training(model)

# LoRAモデルの準備
model = get_peft_model(model, lora_config)

# 学習可能パラメータの確認
model.print_trainable_parameters()
import transformers
eval_steps = 200
save_steps = 200
logging_steps = 20

# トレーナーの準備
trainer = transformers.Trainer(
    model=model,
    train_dataset=train_data,
    eval_dataset=val_data,
    args=transformers.TrainingArguments(
        num_train_epochs=3,
        learning_rate=3e-4,
        logging_steps=logging_steps,
        evaluation_strategy="steps",
        save_strategy="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=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
# 学習の実行
model.config.use_cache = False
trainer.train() 
model.config.use_cache = True

# LoRAモデルの保存
trainer.model.save_pretrained(peft_name)

import torch
from peft import PeftModel, PeftConfig
from transformers import AutoModelForCausalLM, AutoTokenizer

# モデルの準備
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    #load_in_8bit=True,
    device_map="auto",
)

# トークナイザーの準備
tokenizer = AutoTokenizer.from_pretrained(model_name, use_fast=False)

# LoRAモデルの準備
model = PeftModel.from_pretrained(
    model, 
    peft_name, 
    device_map="auto"
)

# 評価モード
model.eval()
# プロンプトテンプレートの準備
def generate_prompt(data_point):
    if data_point["input"]:
        result = f"""### 指示:
{data_point["instruction"]}

### 入力:
{data_point["input"]}

### 回答:
"""
    else:
        result = f"""### 指示:
{data_point["instruction"]}

### 回答:
"""

    # 改行→<NL>
    result = result.replace('\n', '<NL>')
    return result
# テキスト生成関数の定義
def generate(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):]
            print(result.replace("<NL>", "\n"))  # <NL>→改行
        else:
            print('Warning: Expected prompt template to be emitted.  Ignoring output.')
    else:
        print('Warning: no <eos> detected ignoring output')

generate("自然言語処理とは?")
generate("日本の首都は?")
generate("富士山の登山ルートはいくつある?")
generate("光の三原色とは?")
generate("エヴァンゲリオンの登場人物で最も謎が多いのは?")

コツは、パディングをしないこととトークナイザがちゃんと終端記号を処理しているか確認すること。

あとは実行するだけだが、ABCIを使ったのでこうなった

#!/bin/bash

#$-l rt_F=1
#$-j y
#$ -l h_rt=30:00:45
#$-cwd
export TRANSFORMERS_CACHE=/scratch/<自分のID>
source /etc/profile.d/modules.sh
cd /scratch/<自分のID>/git/lora-instruct
module load python/3.10/3.10.10
module load cuda/11.7/11.7.1
module load cudnn/8.4/8.4.1
python3 ftrinna.py

スクラッチパッドでやらないとクォータを使い果たして死ぬので、この手の実験はスクラッチパッドでやることにしてる(ただし、いつデータが消えるかわからないので大事なデータは自分のディレクトリにコピーしてもどしておく)