🌟

Tanuki-8BにMagpieを適用して日本語の合成対話データセットを作成する

2024/10/05に公開

はじめに

本記事では、Tanuki-8BにMagpieという手法を適用してInstruction Tuning用の合成データセットを作成する方法について解説します。
本記事に記載の手法で作成済みの合成データセットを下記リンク先で公開しています。中身を確認したい方はご参照ください。
https://huggingface.co/datasets/Aratako/Magpie-Tanuki-8B-97k

Magpieについて

概要

Magpieは既存のInstruction Tuning済みモデルのみを使い、ゼロからinstructionを合成する手法です。

元論文はこちら
https://arxiv.org/abs/2406.08464

公式の実装はこちら
https://github.com/magpie-align/magpie

以下、Magpieによる合成の流れや、他の合成手法と比較した時の良い点・悪い点について記載します。

合成の流れ

Magpieによる対話データセット合成の流れは非常にシンプルです。公式リポジトリにある以下の画像が手順のすべてを表しています。

以下に合成の流れを解説します。

まずはinstructionを合成します。これは非常にシンプルで、Instruction Tuning済みのLLMに、そのモデルのチャットテンプレートにおいてユーザーのinstructionが入る手前の部分までを入力するだけです。
これだけだと伝わりづらいかもしれませんが、実例を見るとすぐに理解出来ると思います。

例として、Tanukiのモデルを使って解説します。Tanukiは以下のようなチャットテンプレートを利用しています。

<s>以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい。

### 指示:
{instruction}

### 応答:
{response}

ユーザーのinstructionが入る手前の部分までを入力すればよいので、TanukiでMagpieを利用する場合、以下の部分をプロンプトとして与えます。

<s>以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい。

### 指示:

このプロンプトを与えることでモデルはnext token predictionの文脈で本来ユーザーが入力するinstructionを出力します。これにより、ゼロからinstructionを合成することが出来ます。

後はこの合成したinstructionをモデルに通常のチャットと同様に与え、response部分を生成するだけです。response部分は別のモデルで合成しても問題ありません。

他のデータ合成手法との比較

Magpieと、他のLLMを使ったデータ合成手法を比較してみます。

良い点

  • 実装が非常にシンプルで簡単
    データ合成の流れは上述した通り非常にシンプルで、簡単に実装出来ます。
  • 参照元となるシードプロンプトやテキストが不要
    多くのデータ合成手法は既存のinstructionを元に書き換えて新たなinstructionを作成したり、何らかのテキストを参照しながらinstructionを作成しますが、Magpieではその必要がなくLLM単体でゼロから合成が可能です。
  • 計算量が非常に少なく高速に大量のinstructionを合成可能
    参照するテキストが存在しないので入力プロンプトが非常に短く、かつ全て同じプロンプトなので、vLLMのバッチ推論等を利用することで非常に高速にinstructionを合成できます。
    参考として、Tanuki-8Bを利用する場合はRTX 4090を用いてvLLMでバッチサイズ1000のバッチ推論で1回約20秒~25秒程度で処理が出来ました。
  • 既存の対話データセットから2ターン目以降のinstructionを合成することも可能
    上の解説では1ターン目のinstructionを合成していましたが、同様の考えで1ターン目の応対+2ターン目のinstructionの手前までの部分をプロンプトとして入力することで、既存のシングルターンの対話データから2ターン目のinstructionを合成するという事も可能です。
  • instructionのタスクをある程度指定できる
    入力プロンプトを少し変えることで、合成されるinstructionのタスクをある程度指定できます。例えば、### 指示:\nまでではなく### 指示:\n以下の数学に関する質問に回答してください。\nというようなプロンプトを与えることで、合成されるinstructionを数学関連に誘導することができます。同様に、システムプロンプトを変更する事でも誘導が可能です。
  • 同じモデルでresponseも合成した場合、その品質は比較的高い(と推測される)
    この手法は、本質的にモデルのInstruction Tuning時に使われた対話データを蒸留しているのと近いことを行っていると考えられます。つまり、この手法で合成されるinstructionはそのモデルにとってある程度「学習済み」であると考えられ、同じモデルでresponseを合成した場合、その品質も比較的高くなると推測できます。

悪い点

  • モデルによって向き不向きが大きく分かれる
    これは実際に様々なモデルに対してMagpieを適用したことで分かったことですが、モデルによって大きく向き不向きが分かれ、向いていないモデルで合成しても多様性に大きく欠けたinstructionしか合成できません。
    これには、そのモデルのInstruction Tuningのデータの多様性や、instruction部分の学習有無が関係していると考えられます。例えばNemotron-4-340B-Instructではあまり良いinstructionが合成できませんでした。
  • 一部おかしいinstructionが混ざる
    これはどの合成手法にも言える事ですが、合成したinstructionの一部に日本語としておかしな表現や不自然な言い回し等が混ざってしまう事があります。特にMagpieでは他の手法よりもこの問題の頻度が高いと感じます。実用のためには何らかのフィルタリングや言い換え等の処理をする必要があると考えられます。
  • 一部合成が難しいタスクがある
    例えば、「以下の表を元に回答してください」「この文章を要約してください」といったタスクの合成は難しいです。これは、安定した合成を行うために合成時のstop sequenceに改行等を含める必要があり、途中で出力が切られてしまうからです。

Tanuki-8Bについて

今回はMagpieを適用するモデルとしてTanuki-8Bを選びました。

通常のinstructモデルは基本的にInstruction Tuning時に応答部分のみしか学習していませんが、Tanukiはこちらの記事にあるように事前学習時に対話形式のデータを多く学習しており、instruction部分も多く学習しています。そのため、Magpieに向いているモデルである可能性が高いと考え、今回はこのモデルを選びました。

実行コード

vLLMを使い、非常にシンプルなスクリプトで実際にinstructionを合成しました。利用したコードを以下に示します。

利用したコード
"""
TanukiとMagpieを用いて、1ターン目のinstructionを作成するスクリプト
"""

import json
import logging

from tqdm.auto import tqdm
from vllm import LLM, SamplingParams

# ロギングの設定
logging.basicConfig(level=logging.INFO)

# 設定定数
CONFIG = {
    "MODEL_NAME": "weblab-GENIAC/Tanuki-8B-dpo-v1.0",
    "TENSOR_PARALLEL_SIZE": 1,  # 利用環境のGPUの数に合わせる
    "MAX_NUM_SEQS": 1000,
    "MAX_NUM_BATCHED_TOKENS": 4096,
    "MAX_MODEL_LEN": 4096,
    "DOWNLOAD_DIR": "./cache",
    "SAMPLING_PARAMS": SamplingParams(
        temperature=1,
        top_p=1,
        max_tokens=1024,
        repetition_penalty=1.1,
        stop=["\n\n", "###", "assistant", "user", "<EOD>"],
    ),
    "NUM_BATCHES": 10,
    "BATCH_SIZE": 1000,
    "OUTPUT_FILE_NAME": "./tanuki_magpie.jsonl",
    "BACKUP_FILE_NAME": "./backup.jsonl",
    "BACKUP_FREQUENCY": 2,
    "MIN_TEXT_LENGTH": 10,
    "ENDING_PUNCTUATIONS": ["。", ".", "?", "?"],
}


def initialize_model():
    """vLLMでモデルを初期化する"""
    return LLM(
        model=CONFIG["MODEL_NAME"],
        tensor_parallel_size=CONFIG["TENSOR_PARALLEL_SIZE"],
        max_num_seqs=CONFIG["MAX_NUM_SEQS"],
        max_num_batched_tokens=CONFIG["MAX_NUM_BATCHED_TOKENS"],
        download_dir=CONFIG["DOWNLOAD_DIR"],
        max_model_len=CONFIG["MAX_MODEL_LEN"],
    )


def get_prompt():
    """Tanukiのprompt templateに整形する"""
    prompt = """<s>以下は、タスクを説明する指示です。要求を適切に満たす応答を書きなさい。

### 指示:
"""
    return prompt


def process_batch(batch_size, model):
    """vLLMによるバッチ推論を使ったデータ合成"""
    prompts = [get_prompt() for _ in range(batch_size)]
    outputs = model.generate(prompts, CONFIG["SAMPLING_PARAMS"])

    results = []
    valid_outputs = 0
    for output in outputs:
        if not output.outputs:
            continue
        generated_output = output.outputs[0]
        text = generated_output.text.strip()
        if (
            generated_output.finish_reason == "stop"
            and len(text) >= CONFIG["MIN_TEXT_LENGTH"]
            and text[-1] in CONFIG["ENDING_PUNCTUATIONS"]
        ):
            new_data = {
                "messages": [{"role": "user", "content": text}],
                "instruction": text,
            }
            results.append(new_data)
            valid_outputs += 1

    logging.info(f"{batch_size}個のうち{valid_outputs}個の有効な出力を生成しました")
    return results


def save_backup(dataset, file_name):
    """バックアップを保存する関数"""
    with open(file_name, "w", encoding="utf-8") as f:
        for item in dataset:
            json.dump(item, f, ensure_ascii=False)
            f.write("\n")


def create_dataset(model, num_batches, batch_size, backup_frequency, backup_file):
    """バッチサイズごとにデータを処理し、バックアップを保存する"""
    new_dataset = []
    for batch_num in tqdm(range(num_batches)):
        new_data_list = process_batch(batch_size, model)
        new_dataset.extend(new_data_list)

        if (batch_num + 1) % backup_frequency == 0:
            logging.info(f"{batch_num + 1}バッチ目でバックアップを保存します")
            save_backup(new_dataset, backup_file)
        logging.info(f"現在の総データ数: {len(new_dataset)}")

    return new_dataset


def main():
    """メイン処理"""
    model = initialize_model()

    new_dataset = create_dataset(
        model,
        CONFIG["NUM_BATCHES"],
        CONFIG["BATCH_SIZE"],
        CONFIG["BACKUP_FREQUENCY"],
        CONFIG["BACKUP_FILE_NAME"],
    )

    logging.info(f"作成されたデータ数: {len(new_dataset)}")

    # 結果の保存
    with open(CONFIG["OUTPUT_FILE_NAME"], "w", encoding="utf-8") as f:
        for item in new_dataset:
            json.dump(item, f, ensure_ascii=False)
            f.write("\n")


if __name__ == "__main__":
    main()

実際に生成されたinstructionの例

実際に生成されたinstructionの例を先頭から5つ並べてみます。

もし、無限に近いほど多くのリンゴが机の上に置かれていた場合、そのすべての配列や分布に関わらず、「最も頻繁に見られる場所」として特定される位置はありますか?またその理由について詳しく説明してください。ただし、飛び跳ねたり転がったりする物理的な動きはないものとします。
テクニカルライティングと他の形式の文書(例えば、マーケティングや教育用文書など)において共通して必要な基本的なスキルは何ですか?
現在進行中のプロジェクトについて教えていただけますか?どのようなビジネス目標があり、そのためにどんなデータが必要ですか?また、そのプロセスで直面している主な課題は何でしょうか?最後に、あなたの意見として、これらの問題に対する最善のアプローチは何か考えはありますか?
どのような植物があなたの自宅周辺で最もよく見られるものですか?具体的な名前や特徴について教えてください。また、その植物の生態学的役割も考察してみてください。例えば、花粉媒介者としての役割や地域の気候条件への適応などに関して詳しく述べてみてください。
冬季における雪道の安全な運転方法について、具体的なアドバイスを教えてください。特に注意すべきポイントや装備についても詳しく知りたいです。また、雪が少ない地域でも役立つ情報があれば合わせて教えていただけると助かります。

このように、多様で比較的質の良いinstructionが生成できていることが分かります。一方、3つ目の例では架空のプロジェクトを考えることが回答に必要だったり、4つ目の例では「あなたの自宅」という回答者によって回答が異なるものが含まれてしまっているなど、やや問題のある例も確認できます。

実際に学習に利用するには、何らかのフィルタリング処理や言い換え等でデータをクリーニングしてから利用したほうが良いでしょう。とはいえ、非常に小さな計算量でEvol-Instructなどのシードプロンプトが必要な合成手法の元となるようなプロンプトを生成できるので、かなり有用な手法だと思います。

まとめ

本記事では、Tanuki-8BにMagpieという手法を適用してInstruction Tuning用の合成データセットを作成する方法について解説しました。
合成されたinstructionの質は比較的良いですが、実際の利用時には質の良いものだけをフィルタしたり、Evol-Instructのような手法でさらにプロンプトを改善するとより良いinstructionの作成が出来ると考えられます。
元論文では作成されたデータセットに対して様々な可視化や分析、フィルタリング等を行っているので、気になる方は参照してください。

Discussion