LoginSignup
8
5

MetaのオープンソースLLM「Llama 3」を日本語版にファインチューニング(SFT)

Last updated at Posted at 2024-05-27

はじめに

 米Meta(メタ)は米国時間2024年4月18日、次世代の大規模言語モデル(LLM)である「Llama 3」を公開しました。パラメーター数が80億と700億の2つのモデルを用意しました。モデルはオープンソースソフトウエア(OSS)として提供し、より高性能な4000億パラメーターモデルも学習中だとしました。

事前学習モデルをファインチューニング(追加学習)した「Llama 3 Instruct model」は、様々なベンチマークで競合となるLLMの性能を上回りました。例えば700億パラメーターモデルは、米Google(グーグル)が提供する「Gemini 1.5 Pro」や米Anthropic(アンソロピック)の「Claude 3 Sonnet」を凌駕(りょうが)する性能を発揮したとしています。

5兆を超えるトークンの事前学習データを使ってトレーニング済みで、これは前世代モデル「Llama 2」の学習データと比較して7倍の水準です。今後の多言語化対応のため、データセットの5%以上は30以上の言語からなる非英語データで構成しました。ただし、同社は「非英語では英語と同レベルの性能は期待できない」としました。
https://xtech.nikkei.com/atcl/nxt/news/24/00603/

実際に、「Llama 3」は多くの英語ベンチマークで優れた性能を示していますが、主に英語データでファインチューニングされているため、日本語で質問しても英語で回答するか、そもそも日本語の入出力を認識できないことがあるらしいです。ここには、日本語データセットで「Llama 3」をファインチューニングしました。「Llama 3」の日本語能力をアップします。

環境

Colab Pro+, Hugging Face

手順

1.環境準備
2.データセット:Japanese version of the Alpaca dataset
3.乱数固定
4.関数設定
5.Llama3-8B-Instructモデルをダウンロードして、日本語能力を試す
6.ハイパーパラメーター設定
7.ファインチューニング
8.テスト
9.Hugging Faceにプッシュ&結果をダウンロード

参考文献

1,日本語が話せるLlamaモデルをDIYする
https://qiita.com/Taiyou2000/items/3229d320c252d6de33c7

2,Llama-3のファインチューニング(QLoRA)を試す
https://note.com/niki22mk2/n/nd269797a55cb

3,Tagengo: A Multilingual Chat Dataset
https://arxiv.org/abs/2405.12612

4,alpaca_ja
https://github.com/shi3z/alpaca_ja/tree/main

1.環境準備

一般的な処理用:os,sys,json,warnings,logging

可視化用:colorama,tqdm

自然言語処理用:torch,torch.nn,bitsandbytes,datasets,transformers,peft

qiita.rb
""" 必要なPythonライブラリをインストール """
!pip install bitsandbytes==0.43.0
!pip install datasets==2.10.1
!pip install transformers==4.38.2
!pip install peft==0.9.0
!pip install sentencepiece==0.1.99
!pip install -U accelerate==0.28.0
!pip install colorama==0.4.6

# 一般的な処理用
import os
import sys
import json
import warnings
import logging
warnings.filterwarnings("ignore")

# 機械学習・深層学習用
import torch
import torch.nn as nn
import bitsandbytes as bnb
from datasets import load_dataset, load_from_disk
import transformers, datasets
from peft import PeftModel
from colorama import *

# PEFT (Parameter-Efficient Fine-Tuning) 用
from tqdm import tqdm
from transformers import AutoTokenizer, AutoConfig, AutoModelForCausalLM, BitsAndBytesConfig
from transformers import GenerationConfig
from peft import (
    prepare_model_for_int8_training,
    LoraConfig,
    get_peft_model,
    get_peft_model_state_dict,
    prepare_model_for_kbit_training
)

2.データセット:Japanese version of the Alpaca dataset

来源

Alpacaデータセットを日本語化したものです。
Alpacaデータセットは、スタンフォード大学の研究チームが作成した大規模な instruction-following データセットです。
このデータセットは、様々なタスクや質問に対する人間の回答を大量に集めたもので、言語モデルを instruction-following タスクに適応させるためのファインチューニングに用いられます。
Alpacaデータセットを用いたファインチューニングにより、言語モデルはより自然で汎用的な instruction-following 能力を身につけることができます。
このようなモデルは、質問応答システムや対話システムなど、幅広い自然言語処理アプリケーションに応用可能です。
また、Alpacaデータセットはオープンソースであるため、言語モデルの民主化にも貢献しています。

qiita.rb
""" Alpaca日本語データセットをクローン """
!git clone https://github.com/shi3z/alpaca_ja.git

3.乱数固定

ファインチューニングのプロセスでは、ランダム性があります。結果を再現可能にするため、乱数シードを固定します。

qiita.rb
seed = 42
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
torch.manual_seed(seed)
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(seed)

4.関数設定

qiita.rb
# トレーニングデータを生成
def generate_training_data(data_point):
    """
    (1) Goal:
        - This function is used to transform a data point (input and output texts) to tokens that our model can read

    (2) Arguments:
        - data_point: dict, with field "instruction", "input", and "output" which are all str

    (3) Returns:
        - a dict with model's input tokens, attention mask that make our model causal, and corresponding output targets

    (3) Example:
        - If you construct a dict, data_point_1, with field "instruction", "input", and "output" which are all str, you can use the function like this:
            formulate_article(data_point_1)

    """
    # construct full input prompt
    prompt = f"""\
[INST] <<SYS>>
You are a helpful assistant. あなたは役に立つアシスタント。
<</SYS>>

{data_point["instruction"]}
{data_point["input"]}
[/INST]"""
    # count the number of input tokens
    len_user_prompt_tokens = (
        len(
            tokenizer(
                prompt,
                truncation=True,
                max_length=CUTOFF_LEN + 1,
                padding="max_length",
            )["input_ids"]
        ) - 1
    )
    # transform input prompt into tokens
    full_tokens = tokenizer(
        prompt + " " + data_point["output"] + "</s>",
        truncation=True,
        max_length=CUTOFF_LEN + 1,
        padding="max_length",
    )["input_ids"][:-1]
    return {
        "input_ids": full_tokens,
        "labels": [-100] * len_user_prompt_tokens
        + full_tokens[len_user_prompt_tokens:],
        "attention_mask": [1] * (len(full_tokens)),
    }

# 応答を生成して評価する
def evaluate(instruction, generation_config, max_len, input="", verbose=True):
    """
    (1) Goal:
        - This function is used to get the model's output given input strings

    (2) Arguments:
        - instruction: str, description of what you want model to do
        - generation_config: transformers.GenerationConfig object, to specify decoding parameters relating to model inference
        - max_len: int, max length of model's output
        - input: str, input string the model needs to solve the instruction, default is "" (no input)
        - verbose: bool, whether to print the mode's output, default is True

    (3) Returns:
        - output: str, the mode's response according to the instruction and the input

    (3) Example:
        - If you the instruction is "ABC" and the input is "DEF" and you want model to give an answer under 128 tokens, you can use the function like this:
            evaluate(instruction="ABC", generation_config=generation_config, max_len=128, input="DEF")

    """
    # construct full input prompt
    prompt = f"""\
[INST] <<SYS>>
You are a helpful assistant and good at conversation.あなたは役に立つアシスタント、日常会話をするのが得意なアシスタントです。
<</SYS>>

{instruction}
{input}
[/INST]"""
    # プロンプトをモデルが必要な数値表現に変換
    inputs = tokenizer(prompt, return_tensors="pt")
    input_ids = inputs["input_ids"].cuda()
    # 結果を生成
    generation_output = model.generate(
        input_ids=input_ids,
        generation_config=generation_config,
        return_dict_in_generate=True,
        output_scores=True,
        max_new_tokens=max_len,
    )
    # 生成されたレスポンスをデコードしてプリントアウト
    for s in generation_output.sequences:
        output = tokenizer.decode(s, skip_special_tokens=True)
        output = output.split("[/INST]")[1].replace("</s>", "").replace("<s>", "").replace("Assistant:", "").replace("Assistant", "").strip()
        if verbose:
            print(output)

    return output

5.Llama3-8B-Instructモデルをダウンロードして、日本語能力を試す

qiita.rb
"""メタのLlama3-8B-Instructバージョンを使用 """
huggingface_token = "hf_xxxxx"  # 自分のhugging face のReadキーに替えてください

model_name = "meta-llama/Meta-Llama-3-8B-Instruct"

cache_dir = "./cache"

nf4_config = BitsAndBytesConfig(
   load_in_4bit=True,
   bnb_4bit_quant_type="nf4",
   bnb_4bit_use_double_quant=True,
   bnb_4bit_compute_dtype=torch.bfloat16
)

# 事前学習された言語モデルをロード
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    cache_dir=cache_dir,
    quantization_config=nf4_config,
    low_cpu_mem_usage=True,
    use_auth_token=huggingface_token
)

# トークナイザを初期化し、終了記号を設定(eos_token)
logging.getLogger('transformers').setLevel(logging.ERROR)
tokenizer = AutoTokenizer.from_pretrained(
    model_name,
    add_eos_token=True,
    cache_dir=cache_dir,
    quantization_config=nf4_config,
    use_auth_token=huggingface_token
)
tokenizer.pad_token = tokenizer.eos_token

# モデル推論時に使用するデコーディングパラメータを設定
max_len = 128
generation_config = GenerationConfig(
    do_sample=True,
    temperature=0.1,
    num_beams=1,
    top_p=0.3,
    no_repeat_ngram_size=3,
    # pad_token_id=2,
    pad_token_id=tokenizer.eos_token_id,  # 終了記号を設定
    eos_token_id=tokenizer.eos_token_id,  # 添加终止符
    max_length=128  # 最大生成长度
)


qiita.rb
# 日常会話の日本語のサンプル
test_japanese_list = ['こんにちは、元気ですか?', 'お名前は何ですか?', '今日はどんなことがありましたか?']

# 各サンプルに対してモデルの出力を取得
demo_before_finetune = []
for japanese in test_japanese_list:
    demo_before_finetune.append(f'モデル入力:\n以下は日本語の会話です。会話の続きを生成してください。{japanese}\n\nモデル出力:\n' + evaluate('以下は日本語の会話です。会話の続きを生成してください。', generation_config, max_len, japanese, verbose=False))

# 出力結果を表示して保存
for idx in range(len(demo_before_finetune)):
    print(f"Example {idx + 1}:")
    print(demo_before_finetune[idx])
    print("-" * 80)

image.png

分析

微調整前のモデルは、日本語の入力に対して適切な日本語の出力を生成することができていません。具体的には、以下の問題が見られます:

1,日本語入力の認識不足:

モデルは日本語の入力を正しく認識できていない可能性があります。入力に対する出力が期待される日本語の応答ではなく、システムメッセージ(>>SYS>> [INST])になっている点から、この問題が推測されます。

2,日本語出力の生成失敗:

モデルは適切な日本語の出力を生成できておらず、定型のシステムメッセージのみを返しています。これは、モデルが日本語の文脈を理解していないか、日本語での応答を生成する能力が不足していることを示しています。

結論

微調整前のモデルは、日本語の入力に対して適切な応答を生成する能力が不足しています。この結果から、日本語データセットを用いた微調整が必要であることが確認されます。微調整後のモデルでは、適切な日本語の出力が期待されます。

6.ハイパーパラメーター設定

qiita.rb
# トレーニングデータの数を設定
num_train_data = 1040 #5000 # できるだけ多くのデータをトレーニングした場合は出力の質が向上しますが, トレーニング時間も長くなります.
                      # デフォルト値(1040)を使用: FTには約25分かかり, 全セルの完全な実行には約50分かかる.T4GPU
                      # 最大値(5000)を使用: 微調整には約100分かかり, 全セルのフル実行には約120分かかる.T4GPU

# 保存パスをColabのローカルキャッシュに保存
ckpt_dir = "/content/model_ckpt"

# トレーニングに関するパラメータ
num_epoch = 6 #3  # トレーニングの総エポック数を3に設定
LEARNING_RATE = 3e-4 #学習率
qiita.rb
cache_dir = "./cache" #キャッシュのファイルパース
from_ckpt = False # チェックポイントからモデルのウェイトをロードするかどうか, デフォルトはno
ckpt_name = None # 特定のチェックポイントからウェイトをロードする際に利用するファイルの名, デフォルトはない
dataset_dir = "./alpaca_ja/alpaca_cleaned_ja.json" # データセットのディレクトリまたはファイルパスを設定
logging_steps = 20 # トレーニングログを出力するために、トレーニングプロセスのステップ数を定義
save_steps = 65 # トレーニング中にモデルを保存する頻度を定義
save_total_limit = 3 # モデルのチェックポイントを最大保持数
report_to = None # 実験指標のレポート先を設定, デフォルトはない
MICRO_BATCH_SIZE = 4 # マイクロバッチのサイズを定義
BATCH_SIZE = 16 # 1バッチのサイズを定義
GRADIENT_ACCUMULATION_STEPS = BATCH_SIZE // MICRO_BATCH_SIZE # 各マイクロバッチで累積する勾配のステップ数を計算
CUTOFF_LEN = 256 # テキストを切りの長さを設定
LORA_R = 8 # LORA(Layer-wise Random Attention)のR値を設定
LORA_ALPHA = 16 # LORAのAlpha値を設定
LORA_DROPOUT = 0.05 # LORAのDropout率を設定
VAL_SET_SIZE = 0 # 検証セットのサイズを設定、デフォルトはなし
TARGET_MODULES = ["q_proj", "up_proj", "o_proj", "k_proj", "down_proj", "gate_proj", "v_proj"]# ターゲットモジュールを設定、これらのモジュールのウェイトがチェックポイントとして保存
device_map = "auto" # デバイスマッピングを設定、デフォルトは"auto"
world_size = int(os.environ.get("WORLD_SIZE", 1)) # 環境変数"WORLD_SIZE"の値を取得、設定されていない場合はデフォルトで1
ddp = world_size != 1 # world_sizeに基づいて分散データ処理(DDP)を使用するかどうかを判断、world_sizeが1の場合はDDPを使用しない
if ddp:
    device_map = {"": int(os.environ.get("LOCAL_RANK") or 0)} # DDPを使用する場合、ローカルデバイスのマッピングを設定
    GRADIENT_ACCUMULATION_STEPS = GRADIENT_ACCUMULATION_STEPS // world_size # world_sizeに基づいて勾配累積ステップ数を調整

7.ファインチューニング

qiita.rb
# モデルチェックポイント保存ディレクトリを作成

# os.makedirs(output_dir, exist_ok = True)
os.makedirs(ckpt_dir, exist_ok=True)

# from_ckptフラグに基づいてチェックポイントからモデルのウェイトをロード
if from_ckpt:
    model = PeftModel.from_pretrained(model, ckpt_name)

# INT8トレーニングするためにモデルを準備
model = prepare_model_for_int8_training(model)

# LoraConfig を使って LORA モデルを設定
config = LoraConfig(
    r=LORA_R,
    lora_alpha=LORA_ALPHA,
    target_modules=TARGET_MODULES,
    lora_dropout=LORA_DROPOUT,
    bias="none",
    task_type="CAUSAL_LM",
)
model = get_peft_model(model, config)

# トークナイザーのパディングトークンを0に設定
tokenizer.pad_token_id = 0

# トレーニングデータのロードと処理
with open(dataset_dir, "r", encoding="utf-8") as f:
    data_json = json.load(f)
with open("tmp_dataset.json", "w", encoding="utf-8") as f:
    json.dump(data_json[:num_train_data], f, indent=2, ensure_ascii=False)

data = load_dataset('json', data_files="tmp_dataset.json", download_mode="force_redownload")


# 訓練データを訓練セットと検証セットに分割する(VAL_SET_SIZEが0より大きい場合)
if VAL_SET_SIZE > 0:
    train_val = data["train"].train_test_split(
        test_size=VAL_SET_SIZE, shuffle=True, seed=42
    )
    train_data = train_val["train"].shuffle().map(generate_training_data)
    val_data = train_val["test"].shuffle().map(generate_training_data)
else:
    train_data = data['train'].shuffle().map(generate_training_data)
    val_data = None


# トランスフォーマートレーナーによるモデルトレーニング
trainer = transformers.Trainer(
    model=model,
    train_dataset=train_data,
    eval_dataset=val_data,
    args=transformers.TrainingArguments(
        per_device_train_batch_size=MICRO_BATCH_SIZE,
        gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
        warmup_steps=50,
        num_train_epochs=num_epoch,  # トレーニングの総エポック数を3に設定
        learning_rate=LEARNING_RATE,
        fp16=True, # ミキシング精度トレーニングの利用
        logging_steps=logging_steps,
        save_strategy="steps",
        save_steps=save_steps,
        output_dir=ckpt_dir,
        save_total_limit=save_total_limit,
        ddp_find_unused_parameters=False if ddp else None, # DDPを使用して勾配更新を制御するか
        report_to=report_to,
    ),
    data_collator=transformers.DataCollatorForLanguageModeling(tokenizer, mlm=False),
)

# モデルのキャッシュ機能を無効にする
model.config.use_cache = False

# PyTorch バージョン 2.0 以降且つ Windows 以外のシステムを使用している場合、モデルをコンパイル
if torch.__version__ >= "2" and sys.platform != 'win32':
    model = torch.compile(model)


# モデルのトレーニング開始
trainer.train()

# トレーニング完了後、モデルをローカルキャッシュディレクトリに保存
model.save_pretrained(ckpt_dir)

print(f"モデルは次の場所に保存されました: {ckpt_dir}")

# 学習過程でウェイトが欠落する可能性がある場合は警告メッセージを表示
print("\n If there's a warning about missing keys above, please disregard :)")

トレーニング結果の評価

今回のモデルトレーニングにおける結果を以下にまとめます。

1. トレーニングの進捗と損失の減少

トレーニングの進行に伴い、損失(loss)が順調に減少していることが確認できます。具体的には、最初のエポックでの損失が2.3914であったのに対し、最終エポックでは0.164まで減少しています。これは、モデルが効果的に学習を進めており、トレーニングデータに対して精度が向上していることを示しています。

{'loss': 2.3914, 'epoch': 0.31}
{'loss': 1.3642, 'epoch': 0.62}
{'loss': 1.2458, 'epoch': 0.92}
{'loss': 1.1317, 'epoch': 1.23}
{'loss': 1.0785, 'epoch': 1.54}
{'loss': 1.0705, 'epoch': 1.85}
{'loss': 0.9621, 'epoch': 2.15}
{'loss': 0.7578, 'epoch': 2.46}
{'loss': 0.7702, 'epoch': 2.77}
{'loss': 0.6826, 'epoch': 3.08}
{'loss': 0.4726, 'epoch': 3.38}
{'loss': 0.5193, 'epoch': 3.69}
{'loss': 0.4873, 'epoch': 4.0}
{'loss': 0.2865, 'epoch': 4.31}
{'loss': 0.2919, 'epoch': 4.62}
{'loss': 0.2841, 'epoch': 4.92}
{'loss': 0.2025, 'epoch': 5.23}
{'loss': 0.1695, 'epoch': 5.54}
{'loss': 0.164, 'epoch': 5.85}

2. 勾配ノルム(grad_norm)の観察

勾配ノルム(grad_norm)はトレーニング中の勾配の大きさを示す指標であり、モデルの学習の安定性を確認するために重要です。以下の結果から、勾配ノルムが1.998から1.136にかけて変動していますが、全体的には安定していると言えます。

{'grad_norm': 1.998399257659912, 'epoch': 0.31}
{'grad_norm': 0.8518331050872803, 'epoch': 0.62}
{'grad_norm': 0.8703283071517944, 'epoch': 0.92}
{'grad_norm': 0.8187129497528076, 'epoch': 1.23}
{'grad_norm': 0.9557299017906189, 'epoch': 1.54}
{'grad_norm': 0.9686204791069031, 'epoch': 1.85}
{'grad_norm': 1.1744751930236816, 'epoch': 2.15}
{'grad_norm': 1.2353003025054932, 'epoch': 2.46}
{'grad_norm': 1.1154261827468872, 'epoch': 2.77}
{'grad_norm': 0.9254124164581299, 'epoch': 3.08}
{'grad_norm': 1.3947598934173584, 'epoch': 3.38}
{'grad_norm': 1.1954766511917114, 'epoch': 3.69}
{'grad_norm': 1.270248293876648, 'epoch': 4.0}
{'grad_norm': 1.0841108560562134, 'epoch': 4.31}
{'grad_norm': 1.2966179847717285, 'epoch': 4.62}
{'grad_norm': 1.313712239265442, 'epoch': 4.92}
{'grad_norm': 1.2630895376205444, 'epoch': 5.23}
{'grad_norm': 0.9596277475357056, 'epoch': 5.54}
{'grad_norm': 1.1360856294631958, 'epoch': 5.85}

3. 学習率(learning_rate)の推移

学習率は0.00011999999999999999から始まり、トレーニングの進行に伴い徐々に減少しています。これは、トレーニングの進行に応じて学習率を調整することで、過学習を防ぎ、モデルの性能を安定させるための一般的な手法です。

{'learning_rate': 0.00011999999999999999, 'epoch': 0.31}
{'learning_rate': 0.00023999999999999998, 'epoch': 0.62}
{'learning_rate': 0.00029117647058823524, 'epoch': 0.92}
{'learning_rate': 0.00027352941176470583, 'epoch': 1.23}
{'learning_rate': 0.0002558823529411764, 'epoch': 1.54}
{'learning_rate': 0.000238235294117647, 'epoch': 1.85}
{'learning_rate': 0.00022058823529411765, 'epoch': 2.15}
{'learning_rate': 0.00020294117647058822, 'epoch': 2.46}
{'learning_rate': 0.0001852941176470588, 'epoch': 2.77}
{'learning_rate': 0.0001676470588235294, 'epoch': 3.08}
{'learning_rate': 0.00015, 'epoch': 3.38}
{'learning_rate': 0.00013235294117647058, 'epoch': 3.69}
{'learning_rate': 0.00011470588235294115, 'epoch': 4.0}
{'learning_rate': 9.705882352941176e-05, 'epoch': 4.31}
{'learning_rate': 7.941176470588235e-05, 'epoch': 4.62}
{'learning_rate': 6.176470588235294e-05, 'epoch': 4.92}
{'learning_rate': 4.4117647058823526e-05, 'epoch': 5.23}
{'learning_rate': 2.647058823529412e-05, 'epoch': 5.54}
{'learning_rate': 8.823529411764705e-06, 'epoch': 5.85}

4. トレーニング全体の評価

トレーニングは順調に進行し、最終的なトレーニング損失は0.7390924753286899となりました。これにより、モデルの精度が大幅に向上したことが示されています。また、トレーニングサンプルあたりの処理速度は4.955サンプル/秒、トレーニングステップあたりの処理速度は0.31ステップ/秒であり、効率的なトレーニングが実施されたことがわかります。

{'train_runtime': 1259.3295, 'train_samples_per_second': 4.955, 'train_steps_per_second': 0.31, 'train_loss': 0.7390924753286899, 'epoch': 6.0}

結論

今回のトレーニング結果から、モデルは着実に性能を向上させており、学習プロセスも安定していることが確認できました。特に、エポックごとの損失の減少や勾配ノルムの安定性は、モデルが効果的に学習している証拠です。

モデルは /content/model_ckpt に保存され、今後の応用や追加の微調整に使用できます。この結果を基に、さらに改良を加え、モデルの性能を一層向上させることが期待されます。

最後に、以下のような改善点や次のステップを考慮すると良いでしょう:

  1. データセットの多様化
    より多くの多様なデータセットを用いることで、モデルの汎用性を高める。

  2. ハイパーパラメータの調整
    学習率やバッチサイズなどのハイパーパラメータを最適化し、さらに効率的なトレーニングを目指す。

8.テスト

ファインチューニングのプロセスが完了しました。次に、以前はできなかった目的のタスクを、ファインチューニング後のモデルが実行できるかどうかをテストします。

qiita.rb
# 利用可能なすべてのチェックポイントをプットアウト
ckpts = []
for ckpt in os.listdir(ckpt_dir):
    if ckpt.startswith("checkpoint-"):
        ckpts.append(ckpt)

# すべてのチェックポイントを表示
ckpts = sorted(ckpts, key=lambda ckpt: int(ckpt.split("-")[-1]))
print("利用可能なすべてのチェックポイント:")
print(" id: checkpoint name")
for (i, ckpt) in enumerate(ckpts):
    print(f"{i:>3}: {ckpt}")
qiita.rb
# モデルチェックポイントの選択
id_of_ckpt_to_use = -1  # 推論に使用するcheckpointのid(-1は後ろから一番目)

ckpt_name = os.path.join(ckpt_dir, ckpts[id_of_ckpt_to_use])
qiita.rb
max_len = 128     # 生成の最大長さ
temperature = 0.1  # 生成のランダム性を設定、値が小さいほど生成された応答が安定する
top_p = 0.3 # Top-p (nucleus) サンプリングの確率閾値を設定し、生成された応答の多様性を制御
top_k = 5  # Top-kの値を調整し、生成された応答の多様性を増やし、重複した語彙の生成を避ける
qiita.rb
# 保存パース
output_dir = "/content"  


# テストデータのパスと出力パス
test_data_path = "alpaca_ja/alpaca_cleaned_ja.json"
output_path = os.path.join(output_dir, "results.txt")

cache_dir = "./cache"  # キャッシュディレクトリのパスを設定
seed = 42  # ランダムシードを設定し、結果を再現可能にする
no_repeat_ngram_size = 3  # 重複Ngramを禁止するサイズを設定し、重複したフレーズの生成を避ける

nf4_config = BitsAndBytesConfig(
   load_in_4bit=True,
   bnb_4bit_quant_type="nf4",
   bnb_4bit_use_double_quant=True,
   bnb_4bit_compute_dtype=torch.bfloat16
) # nf4の設定を構成し、4bitのロードと計算に使用


# トークナイザの再初期化と生成設定、モデル名をトークナイザでモデルが読み取れる数値表現に変換
tokenizer = AutoTokenizer.from_pretrained(
    model_name,
    cache_dir=cache_dir,
    quantization_config=nf4_config
)

# 事前訓練済みモデルをロードし、8ビット整数 (INT8) モデルとして設定
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=nf4_config,
    device_map={'': 0},  # 使用するデバイスを設定、ここではGPU 0を指定
    cache_dir=cache_dir
)

# 指定されたチェックポイントからモデルのウェイトをロードする
model = PeftModel.from_pretrained(model, ckpt_name, device_map={'': 0})
qiita.rb
# 日本語会話性能評価
results = []

# 生成設定を構成し、ランダム性、ビームサーチなどの関連パラメータを含む
generation_config = GenerationConfig(
    do_sample=True,
    temperature=0.1,
    num_beams=1,
    top_p=0.3,
    top_k=top_k,
    no_repeat_ngram_size=3,
    pad_token_id=tokenizer.eos_token_id,  # 終了記号を設定
    eos_token_id=tokenizer.eos_token_id,  # 終止符を添加
)

# テストデータを読み込む
with open(test_data_path, "r", encoding="utf-8") as f:
    test_datas = json.load(f)

# 各テストデータに対して予測を行い、結果を保存
with open(output_path, "w", encoding="utf-8") as f:
    for (i, test_data) in enumerate(test_datas):
        if i >= 15:
            break
        predict = evaluate(test_data["instruction"], generation_config, max_len, test_data["input"], verbose=False)
        f.write(f"{i+1}. " + test_data["input"] + predict + "\n")
        print(f"{i+1}. " + test_data["input"] + predict)

テスト集の5つを用いて、日本語生成能力をテスト:

"健康維持のための3つのコツを教えてください。"

1、バランスのとれた食事を摂り、野菜や果物を十分に摂ること。2、定期的に運動をして体の活力を保つこと。3、睡眠時間を十分にとり、規則正しい睡眠をとること。
4、ストレスを軽減するためにリラクゼーション・テクニックを練習すること。5、水を十分飲み、体をきれいにすること。6、医師の推奨でる健康食品を摳ること

"三原色とは何ですか?"

  1. 三原色は赤、青、黄です。
    三原色的別名は、黄、青、および赤です。これらは、他の色を構成するために使用される基本的な色です。三原子は、色の学を構築するために不可欠な要素であるという点で、重要です。また、これらの色は、組み合わせによって形成される色の多様性を説明するのに役立ちます。Mixture of three primary colors can create a wide range of hues and shades, and is the foundation of color theory.

"原子の構造を説明せよ。"

  1. 原子は、陽子と中性子を含む原子核と、その周りを周回する電子で構成されています。陽子および中性子は正電荷を持ち、電子は負電荷を持つため、全体としては中性の原子になります。各粒子の数によって、原子番号と原子の種類が決まります。

"どうすれば大気汚染を減らせるでしょうか?"

  1. 大気汙染を削減するには、再生可能エネルギーへの転換、公共交通機関の利用促進、化石燃料の燃焼禁止、産業排出源からの排出削除、自動車排出基準の実施などの方法があります。また、自動车の使用削减、木材などの燃焼物質の回避、エネルгиー効率の高い器具への交換など、個人が大気の質を改善するのに役立つ行動もあります。

"建設会社のプロジェクト・マネージャーのふりをして、難しい決断をしなければならなかったときのことを説明してください。"

  1. 私は、建設会社でプロジェクマネージァーをしていた時、クライアントの期待に応えるために、ある日までに完成させる必要のあるプロジェктを担当していたので、難しくて大変な決断が必要になりました。予期せぬ遅延があり、締め切りに間に合わず、難しさを増させることから、プロジェットの期限を延長することにしました。私には、クレイアントにプロジェッツの期 限を延长することを求める電話をしったら大変で
qiita.rb
# 同じサンプルを使用して推論を実行
test_japanese_list = ['こんにちは、元気ですか?', 'お名前は何ですか?', '今日はどんなことがありましたか?']

# 微調整後のモデルで推論
demo_after_finetune = []
for japanese in test_japanese_list:
    demo_after_finetune.append(f'モデル入力:\n以下は日本語の会話です。会話の続きを生成してください。{japanese}\n\nモデル出力:\n' + evaluate('以下は日本語の会話です。会話の続きを生成してください。', generation_config, max_len, japanese, verbose=False))

# 出力結果を表示して保存
for idx in range(len(demo_after_finetune)):
    print(f"Example {idx + 1}:")
    print(demo_after_finetune[idx])
    print("-" * 80)

image.png
image.png

9.Hugging Faceにプッシュ&結果をダウンロード

qiita.rb
# 結果ファイルをダウンロード
from google.colab import files
files.download(output_path)
qiita.rb
import os
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

# Hugging Face アクセストークンを用いて認証
huggingface_token = "hf_xxxxx"  # ご自分の Hugging Face Writeアクセストークンに置き換えてください

model_name = "meta-llama/Meta-Llama-3-8B-Instruct"
cache_dir = "./cache"

# キャッシュディレクトリが存在するかどうかを確認
if not os.path.exists(cache_dir):
    os.makedirs(cache_dir)

# 量子化設定を読み込む
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4"
)

# モデルとトークナイザーを読み込む
tokenizer = AutoTokenizer.from_pretrained(model_name, cache_dir=cache_dir, token=huggingface_token)
model = AutoModelForCausalLM.from_pretrained(model_name, cache_dir=cache_dir, quantization_config=quantization_config, token=huggingface_token)

# モデルとトークナイザーをローカルディレクトリに保存
model_output_dir = "./model_output"
model.save_pretrained(model_output_dir)
tokenizer.save_pretrained(model_output_dir)

# 自分でGitリポジトリを初期化してリモートリポジトリを追加
os.system(f"cd {model_output_dir} && git init")
os.system(f"cd {model_output_dir} && git remote add origin https://huggingface.co/UID/Meta-Llama-3-8B-Instruct-jp")
os.system(f"cd {model_output_dir} && git pull origin main")

# ファイルを追加してコミット
os.system(f"cd {model_output_dir} && git add .")
os.system(f"cd {model_output_dir} && git commit -m 'Upload model and tokenizer'")

# リモートリポジトリにプッシュ
os.system(f"cd {model_output_dir} && git push origin main")

# もしくは、HfApiを使用して直接プッシュ
from huggingface_hub import HfApi

api = HfApi()
api.upload_folder(
    folder_path=model_output_dir,
    path_in_repo=".",
    repo_id="UID/Meta-Llama-3-8B-Instruct-jp",
    repo_type="model",
    token=huggingface_token
)

トレーニングが完了した後、モデルはHugging Faceのリポジトリにアップロードされました。以下のコミット情報はその詳細を示しています:

この情報は、モデルが正しくアップロードされ、Hugging Faceプラットフォーム上で共有されたことを確認するために有用です。アップロードされたモデルは今後の研究や開発に活用される予定です。

まとめ

今回のモデルトレーニングとアップロードプロセスを通じて、モデルの性能を向上させるとともに、Hugging Faceリポジトリを活用することで、モデルの共有と再利用が容易になりました。このようなプロセスは、今後のプロジェクトにも役立つ重要なステップです。

考察

一、ハイパーパラメーター:num_train_dataとnum_epochの設定

大量のデータを少ないエポック数で学習すると、モデルは各データポイントを十分に学習する前に次のデータに進んでしまう可能性があります。この現象から、超パラメータ調整の際に以下のような指針が得られます:

  1. エポック数の優先的な増加
    既存のデータ量でまずエポック数を増やすことを試み、モデル性能の変化を観察する。この方法で、モデルが現有のデータを十分に学習できるようにします。

  2. データ量とエポック数のバランス
    学習データ量を増やす場合、エポック数も相応に増やす必要があります。例えば、データ量を倍増させるなら、エポック数も倍にすることで、新しいデータを十分に学習させることができます。データ量のみを増やしてエポック数を増やさないと、モデルの性能が低下する可能性があります。

  3. 段階的な最適化
    学習データ量やエポック数を一度に大幅に増やすのではなく、段階的に増やし、各段階で性能を評価することが重要です。これにより、最適なバランスを見つけやすくなります。

具体的な例

  • num_train_data=1040, num_epoch=3 の設定は、num_train_data=5000, num_epoch=3 よりも良好な結果を得られました。これは、少ないデータ量で各データポイントが十分に学習されるためです。
  • num_train_data=1040, num_epoch=6 の設定は、さらに良好な結果をもたらしました。エポック数を増やすことで、データがより深く学習され、モデルの性能が向上しました。

2、生成の内容まだ堅苦しい:

最適化の手順

  1. エポック数を増やす
    現在のデータ量でエポック数を増やし、性能の向上を確認します。

  2. データ量とエポック数の両方を増やす
    データ量を増やす場合、エポック数も同時に増やします。例えば、データ量を2倍にする場合、エポック数も2倍に設定します。

  3. 段階的に最適化
    変更は段階的に行い、各段階で性能を評価して最適な設定を見つけます。

これらの手法により、学習データ量とエポック数のバランスを取ることができ、モデルの性能を最大限に引き出すことができます。

二、もっと自然な文章生成のために

  1. データセットの改善
    現在使用しているデータセットは翻訳されたものであるため、モデルの日本語能力に限界があるかもしれません。次は、openchat/openchat_sharegpt4_dataset(日本語の会話のみ、167件の会話)と合わせて試して見ます。このデータセットは人間がGPT-4と会話した内容を含んでおり、より自然な日本語の表現が期待できます。

  2. 他の事前学習モデルの利用
    現在使用しているモデルに加えて、nvidia/Llama3-ChatQA-1.5-8B のような他の事前学習モデルも試します。このモデルは会話に最適化されており、日本語会話のタスクに対して異なる性能を発揮できるかもしれません。

  3. ハイパーパラメータの再調整
    モデルの生成品質をさらに向上させるために、以下のハイパーパラメータの再調整を行います:

    • Temperature(0から1):生成されるテキストのランダム性を制御します。低い値はより保守的で一貫性のある出力を、高い値はより多様で創造的な出力を生成します。
    • Top-K(正整数):生成する単語の候補を上位K個に制限します。これにより、より高品質な単語が選ばれやすくなります。
    • Top-P(0から1):確率の合計がP以下になるまで単語の候補を絞り込みます。これにより、極端な選択を避けながら多様性を持たせることができます。
    • Max-length(GPUのメモリ制限に依存):生成するテキストの最大長を設定します。メモリ制限を考慮しながら最適な長さを設定することで、効率的にリソースを使用できます。

これらの改善を実施することで、より自然で流暢な日本語の文章生成が期待でき、モデルのパフォーマンスも向上するでしょう。

今後の計画

  1. unslothの利用
    unslothを活用して、より高度なモデルの微調整やデータ管理を行います。これにより、モデルの性能と効率をさらに向上させることが期待できます。

  2. RLHF(Reinforcement Learning from Human Feedback)による報酬モデルとPPO
    megagonlabs/instruction_ja(669件の会話を含む)を使用して、PPO(Proximal Policy Optimization)アルゴリズムを用いたRLHFを実施します。このデータセットはkunishou/hh-rlhf-49k-jaの翻訳を元にしたもので、手作業で編集されています。これにより、もっと自然な人間らしい応答を生成するモデルを構築することを目指します。

  3. LangChainによるRAG(Retrieval-Augmented Generation)
    LangChainを用いてRAG(Retrieval-Augmented Generation)を実装し、モデルが大規模なデータベースから必要な情報を引き出して活用できるようにします。これにより、応答の精度と関連性が向上し、より有用な情報を提供できるようになります。
    また、LangSmith機能を用いて、効果を評価し、さらなる改善と繋がります。

  4. キャラクターによるAIカウンセラー
    特定のキャラクターを持つAIカウンセラーを開発します。これにより、ユーザーに対してより親しみやすく、個別化された支援を提供できるようになります。キャラクターの設定により、ユーザーとのインタラクションが深まり、より効果的なカウンセリングが可能になります。

以上の工程により、ユーザーに対してより有益でパーソナライズされたサービスを提供することを図ります。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5