LLMだけでデータセット生成してみよう!Magpie方式でのprompt生成

こんにちは、クロガネです。
タイトルの通り、完全にLLMのみを使用したデータセット作成を行います。

以下の論文に基づいて進め行きます。
[2406.08464] Magpie: Alignment Data Synthesis from Scratch by Prompting Aligned LLMs with Nothing (arxiv.org)
本当にそんなに簡単にデータセット生成ができるのか、ということを確認していきましょう。

序論(読み飛ばしてよい)

データセットの作成は、LLMをやるうえで非常に重要な点です。事前学習だけではなく、あとからfine-tuningしたりと、何かと必要になります。

こういう話をするといつも出てくるのが、日本語データセットが一生無いっていうことです。海外のデータセットは健全なものから怪しいものまで幅広く公開されてますが、日本語データセットはアクセスが困難であったり、ライセンスが厳しかったり、そもそも数が無かったりと、集めるのに苦労します。

なので、最近では日本語コミュニティ内でデータセット作成の流れが盛り上がっていますね。

論文にも書いてありますが、データセットの作成方法には手動で作るか、合成して作るかの2通りあります。既存のものではカバーしきれない、完全オリジナルなタスクをこなす場合は、正直自力で作るしか解決策はありません。
もしくは、AITuberのような回答自体に独自性が求められる場合、どうしようもありません。

これとは別に、合成データセットというものがあります。AIが自然な言葉を話せるなら、AIにデータセットを作らせればええやんけ、というものです。
有名なのはShareGPTですね。これはChatGPTの出力を集めたものであり、人間の質問と高性能なChatGPTの高品質な回答がセットになった魅力的なデータセットです。
(ちなみにだいぶリスキーなのでShareGPTは使わないことが身のためです。ChatGPTの利用規約にある、ChatGPTの出力を競合のLLMの学習のために使用してはいけない、というものをバリバリに破ることになります。)

そして、データセットは数年内に枯渇する、と言われています。
Will We Run Out of Data to Train Large Language Models? (epochai.org)

情報の枯渇とそもそも日本語データセットがない、という問題点から、合成データセットはかなり注目されてるというわけです。

本題

ともかく、やってみましょう。

今回はMagpieの公式リポジトリのdemoを改造したコードを使用します。
magpie-align/magpie (github.com)

ここのdemo.ipynbを参考にすれば、このnoteは読まなくても問題ありません。


やってることはバリバリシンプルです。

System Promptのみを入力した状態で続きの文章を生成させるだけです。

はい。内容は終わりです。
内容はシンプルですが、論文の結果を見る限り十二分に性能が出るので、もうこれでいいのでは?って感じがしますね。

やってみよう

というわけで、論文の内容の汎用性を高めるため、transformersのpipelineではなく、AutoModelForCausalLMを使用したコードが以下になります。

import json
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig

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

tokenizer = AutoTokenizer.from_pretrained(
    model_id,
    add_special_tokens=True,
    cache_dir=r"I:\llm\cache",
    )

model = AutoModelForCausalLM.from_pretrained(
    model_id, 
    device_map="cuda:0",
    torch_dtype=torch.float16,
    cache_dir=r"I:\llm\cache",
    )

config_setting = AutoConfig.from_pretrained(
    model_id,
    add_special_tokens=True,
    cache_dir=r"I:\llm\cache",
    )


if tokenizer.chat_template is None:
    tokenizer.chat_template = tokenizer.default_chat_template
    print("chat_template is None. Overlay with default_chat_template in chat_template.")

if not "system" in tokenizer.chat_template and "system" in tokenizer.default_chat_template:
    print("system template is not in chat_template but in default_chat_template. Overlay with default_chat_template in chat_template.")
    tokenizer.chat_template = tokenizer.default_chat_template



# use prompt開始のトークンだけを使いたいので、system+userのログを作成した後、指定したランダムな文字列で分割して前半を使います。
s_split = "pAkiKqTVMAymgaRHyP4A"

chat = [
    {
        "role": "system", 
        "content": "あなたはAIアシスタントで、数学の問題を解くために役立つ、ステップバイステップのガイダンスを提供するように設計されています。",

    },
    {
        "role": "user", 
        "content": s_split,
    },
]

tokenizer.use_default_system_prompt = False
extract_input = tokenizer.apply_chat_template(chat, tokenize=False)

extract_input = extract_input.split(s_split)[0]
print("------------\n" + extract_input + "\n------------")


inputs = tokenizer(
    extract_input, 
    return_tensors="pt",
    add_special_tokens = False,
    )
print(inputs)


terminators = [
    tokenizer.eos_token_id,
    tokenizer.convert_tokens_to_ids("<|eot_id|>"),
]
print(terminators)


inputs = inputs.to(model.device)

with torch.no_grad():
    tokens = model.generate(
        **inputs,
        max_new_tokens=1000,
        do_sample=True,
        eos_token_id=terminators,
        temperature=1,
        top_p=1,
    )
    
    
output = tokenizer.decode(tokens[0])
print(stopper.format(output))


コードの流れ

基本的に、通常の推定と同じです。
一工夫してあげるだけで、chat開始のトークンは以下のような形で取り出すことができます。

s_split = "pAkiKqTVMAymgaRHyP4A"

chat = [
    {
        "role": "system", 
        "content": "あなたはAIアシスタントで、数学の問題を解くために役立つ、ステップバイステップのガイダンスを提供するように設計されています。",

    },
    {
        "role": "user", 
        "content": s_split,
    },
]

extract_input = tokenizer.apply_chat_template(chat, tokenize=False)
extract_input = extract_input.split(s_split)[0]

inputs = tokenizer(
    extract_input, 
    return_tensors="pt",
    add_special_tokens = False,
    )

pAkiKqTVMAymgaRHyP4Aみたいな重複しないような文字列をチャット履歴に入れて、chatテンプレートを適用後に分割するだけです。
もちろんこんな事せずに、文字列でハードコーディングしてあげても何ら問題ありません。

tokenizerで入力をtoken化するときにadd_special_tokens = Falseを追加することが肝です。userを示すtokenをこちらでコントロールしたいからです。

参考程度に、chatのテンプレートからuserの開始トークンを取り出す方法もあります。tokenizer.chat_templatetokenizer.default_chat_templateあたりからテンプレートをチェックすることができます。

Mistral対応

Mistralに対応するには、2工夫必要です。

import json
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
from gstop import STOP_TOKENS_REGISTRY


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

tokenizer = AutoTokenizer.from_pretrained(
    model_id,
    add_special_tokens=True,
    cache_dir=r"I:\llm\cache",
    )

model = AutoModelForCausalLM.from_pretrained(
    model_id, 
    device_map="cuda:0",
    torch_dtype=torch.float16,
    cache_dir=r"I:\llm\cache",
    )

config_setting = AutoConfig.from_pretrained(
    model_id,
    add_special_tokens=True,
    cache_dir=r"I:\llm\cache",
    )





if tokenizer.chat_template is None:
    tokenizer.chat_template = tokenizer.default_chat_template
    print("chat_template is None. Overlay with default_chat_template in chat_template.")

if not "system" in tokenizer.chat_template and "system" in tokenizer.default_chat_template:
    print("system template is not in chat_template but in default_chat_template. Overlay with default_chat_template in chat_template.")
    tokenizer.chat_template = tokenizer.default_chat_template



# use prompt開始のトークンだけを使いたいので、system+userのログを作成した後、指定したランダムな文字列で分割して前半を使います。
s_split = "pAkiKqTVMAymgaRHyP4A"

chat = [
    {
        "role": "system", 
        "content": "あなたはAIアシスタントで、数学の問題を解くために役立つ、ステップバイステップのガイダンスを提供するように設計されています。",     
    {
        "role": "user", 
        "content": "USER:\n" + s_split,
    },
]

tokenizer.use_default_system_prompt = False
extract_input = tokenizer.apply_chat_template(chat, tokenize=False)
print("system template is found.")

print(extract_input)

extract_input = extract_input.split(s_split)[0]
print("------------\n" + extract_input + "\n------------")



inputs = tokenizer(
    extract_input, 
    return_tensors="pt",
    add_special_tokens = False,
    )
print(inputs)


terminators = [
    tokenizer.eos_token_id,
    tokenizer.convert_tokens_to_ids("<|eot_id|>"),
]
print(terminators)


inputs = inputs.to(model.device)

stopper = GenerationStopper(STOP_TOKENS_REGISTRY["mistral"])

with torch.no_grad():
    tokens = model.generate(
        **inputs,
        max_new_tokens=1000,
        do_sample=True,
        eos_token_id=terminators,
        stopping_criteria=stopper.criteria,
        temperature=1,
        top_p=1,
    )
    
    
output = tokenizer.decode(tokens[0])
# print(output)
print(stopper.format(output))

gstopの導入

今回は、生成を途中で止めるため、gstopを使用します。これは、通常の eos_token_idに追加で、特定の文字列が生成されたら生成を停止させることができます。
ksterx/gstop: Generation Stopping Criteria for transformers Language Model (github.com)
詳細はこちらから確認してください。
🤗transformersで特定の文字列が出力されたときに生成を止めたい (zenn.dev)

導入はpipから完了します。

pip install gstop

実はmistralv0.2までのinstructモデルで使用されている[/INST]は1 tokenとして登録されておりません。なので、Magpieで想定されている、user側の発言終了を示すtokenがなく、mistral系ではそのままコードを転用すると、assistantの返答が終わって</s>が出るまで延々と文章が生成されます。

なので、gstopを使用して、[/INST]などが生成された段階で止めてあげる必要があります。

from gstop import STOP_TOKENS_REGISTRY
stopper = GenerationStopper(STOP_TOKENS_REGISTRY["mistral"])

stoppermodel.generateのstopping_criteriaに入れてあげるだけです。

with torch.no_grad():
    tokens = model.generate(
        **inputs,
        max_new_tokens=1000,
        do_sample=True,
        eos_token_id=terminators,
        stopping_criteria=stopper.criteria,
        temperature=1,
        top_p=1,
    )

もしくはstreamで生成して中断してあげる手もあるかもしれません。

Promptなどへの「User:」などの追加

Mistral系には、[/INST]以外のUser:などを含めてあげる必要があります。
にもかかわらず、chatテンプレートに含まれていないことが多々あります。

なので、その辺を追加するためには、単純にchat履歴に追加するだけです。

chat = [
    {
        "role": "system", 
        "content": "あなたはAIアシスタントで、数学の問題を解くために役立つ、ステップバイステップのガイダンスを提供するように設計されています。",     
    {
        "role": "user", 
        "content": "USER:\n" + s_split,
    },
]

モデルによっては[User]とか<<USER>>の場合もあります。


生成例

生成例は以下のリポジトリに置いています。
kurogane/Magpie_aixsatoshi_Llama-3-youko-8b-instruct-chatvector_prompt_extract_example50 · Datasets at Hugging Face
kurogane/Magpie_RakutenAI_prompt_extract_example50 · Datasets at Hugging Face

aixsatoshi/Llama-3-youko-8b-instruct-chatvectorで生成したもの

ユークリッドの互除法で最大公約数を求める方法を教えてください。<|eot_id|>
次の問題にアプローチしてください: 7の因数分解をしてください。 7 = (a + b) × (a - b) 7 = 6a - b 0 = 6a - 1
数学の問題です。解きなさい。 2x^23x-4の因数分解を求めよ。 xは、-1以上である。<|eot_id|>
x^2+3x-4=0を解き、解の絶対値を求めなさい。<|eot_id|>
x^2 + 3x + 2 = 0 を解く方法を教えてください。 xの乗数を分母に持つ分数の加減乗除法を使用して、x^2 + 3x + 2 = 0 を解きましょう。 Step 1: x^2 + 3x + 2 = 0 を x^2 + ax + b = 0 の形に変形します。 x^2 + 3x + 2 = 0 を x^2 + 3x + 2 = x^2 + 2 + 3x + 2 - 2 = x^2 + 3x + 2 - 2 をすると、x^2 + ax + b = 0 の形に変形できます。 a = 3, b = 2

Rakuten/RakutenAI-7B-chatで生成したもの

与えられた数学的問題を解くための最も効率的な方法を教えてください。
入力されたテキストは以下のものを認識しません:{0}. 入力させてください。
与えられたテキストを元に数学の問題を出し計算してください。 テキストから問題を出してみました。解くときにガイダンスを発行してください。
与えられた問題を解くために、ステップバイステップのガイダンスを記述します。 問題:2個のボールはそれぞれ3個のココナッツを消費する。4個のボールは何個のココナッツを消費する?
次の数式を解きなさい: - x² + 1 = 2x

それぞれ変なものも交じりつつ、比較的自然な日本語で生成できています。

Instructでないモデルでやってしまったやつ。
kurogane/Magpie_llama-3-youko-8b_prompt_extract_example50 · Datasets at Hugging Face

こんにちは、AIアシスタントです。私は数学の問題を助けることができます。例えば、次の問題を解いてみてください。xは3をかけたもので、yは2xです。3yの式で解いてください。 回答: 3x + 3y = 3(3x + 2x) = 9x + 6x = 15xです。 <|im_end|>
ここでは、ユーザーがAIアシスタントに質問し、質問と答えを表示します。質問と答えは、日本語で表示される必要があります。 ユーザーが、AIアシスタントに「2x + 4 = 5x - 2」という式を解くように質問します。 AIアシスタントが、問題を解くためのステップバイステップのガイダンスをユーザーに見せます。 「2x + 4 = 5x - 2」を解くために必要なステップは次の通りです: ステップ1:左辺と右辺を合わせます。 ステップ2:両辺から4を減らします。 ステップ3:両辺をxで割ります。 ステップ4:最後に、両辺に2を追加します。 ステップ1: 2x + 4 = 5x - 2 ステップ2: 2x = 5x - 6 ステップ3: x = 3 ステップ4: 2x + 4 = 3 + 4 ステップ5: 6 = 7 ユーザーは「解答が正しい」という正のフィードバックをAIアシスタントに返すことができます。 回答を入力するには、ユーザーは以下の方法のいずれかで<|input|>フィールドを使用します。 ユーザーはキーボードを使用して文字を入力し、フィールドを離すことで回答を入力することができます。 AIアシスタントは、ユーザーからの回答を理解し、適切なアクションを実行します。上記の例では、ユーザーが「解答のフィードバックをありがとう」という返事を返すと、AIアシスタントは「良かったよ。あなたの解答は正しかったね!」と返すことができます。 フィードバックを受け取ると、AIアシスタントはユーザーとの会話を終了します。ユーザーが次の質問をするたびに、新たな会話が始まります。 ステップ1:「2x + 4 = 5x - 2」という式を解くには、どのようなプロセスが必要ですか? ステップ2:正のフィードバックと正のフィードバックを受け取った場合 回答を示すには、<|output|>タグが必要です。このタグは、ユーザーの入力と比較して正しい場合にのみ表示されます。また、<|output|>タグを複数使用することができます。
ユーザー名:<|im_start|>jane</|im_start|>さん(女性) こんにちは!AIアシスタントの
ユーザー: 2x - 4 = 0 を解くと、 |2x| = 4になる。|2x|が答えである。しかし方程式には正と負の2つの解がある。それらを区別するため、次の形式で返答します。 <|im_end|>
あなたは中高校生で、数学の試験のために数学のテストの勉強をしています。数学の問題を解くことは難しいですが、頑張っています。AIアシスタントがいろいろ教えてくれるので、とても助かっています。あなたは数学の概念をもっと理解して、方程式を解いて、試験で良い点数を取ることを目標としています。質問と回答は日本語で行われる必要があります。<|im_end|>

まともに生成できてないですね。システムプロンプトの続きを単に生成しているだけのように見えます。


おわりに

以上です。
非常にシンプルな手法ですが、結構便利に感じました。
それこそ、日本語継続学習モデルがいろいろ出ていますので、日本語データを合成するためにはいい手法のように感じます。

生成されたプロンプトをもとに、evol instructなどの手法によりpromptを強化していくことで、さらなる高度なデータセット生成が望めるかもしれません。
また、論文を読む限り、幅広い分野もpromptを割と均等に生成できることがメリットのようですから、seed promptとしても便利に感じます。

いろいろ試してみると面白そうですね。

それでは!クロガネでした!

この記事が気に入ったらサポートをしてみませんか?