LLMを使ってDiscussionの合成データを作成する手順

未分類

こんにちは。今回は 合成データの作成 をテーマに、PythonとLLM(大規模言語モデル)を活用して議論風の対話データを自動生成してみる方法をご紹介します。合成データを作ることで、実際の人間同士のやり取りを再現したい場合や、対話モデルの学習用データを増やしたいときに役立ちます。

プロジェクト概要

  • 2人のキャラクター(Assistant 1とAssistant 2) が異なる主張を持ち、議論を交わす会話データを生成する
  • 生成された会話データを JSONファイル として保存する

具体的には、エンタメの話題(アイドル、俳優、芸人など)をトピックにして、2人の登場人物が議論を行い、どちらかが「結論(賛成・同意)」の形で相手に寄り添ったら議論終了。といった流れを複数のトピックで行い、その結果をまとめます。

使用するツールとライブラリ

  • vLLM: 高性能な大規模言語モデル用ライブラリ
  • Transformers: トークナイザやモデルの操作に使用
  • JSON: 生成されたデータの保存に使用
  • Google Colaboratory (L4)

必要なライブラリを以下のコマンドでインストールします。

!pip install vllm triton

インポートとモデルの準備

import json
from transformers import AutoTokenizer
from vllm import LLM, SamplingParams
  • json: 生成した議論データをJSONで書き出すために使う
  • transformers: Hugging Face Transformersライブラリ。AutoTokenizer を通じてトークナイザーを自動で取得
  • vllm: LLM クラスと SamplingParams を使用し、テキスト生成を行う
model_name = "tokyotech-llm/Llama-3.1-Swallow-8B-Instruct-v0.2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
llm = LLM(model=model_name, tensor_parallel_size=1)
  • model_name に使用したいモデル名を指定
  • AutoTokenizer.from_pretrained(...) でトークナイザーを取得
  • LLM(...) でvLLMのモデルインスタンスを作成

ここでは「Llama-3.1-Swallow-8B-Instruct-v0.2」を例として使っています。東京科学大学が開発した日本語能力が強化されたLlama3.1ベースの強力なモデルです。ご自身の環境に合わせてモデルを変えてもOKです。

サンプリング設定

sampling_params = SamplingParams(
    temperature=1.2,  # バリエーションを増やす
    top_p=0.9,
    max_tokens=512,
    stop="<|eot_id|>"
)
  • temperature: 生成するテキストの「創造性」を調整(値を大きくするとより多様な出力)
  • top_p: nucleus sampling によるサンプリングの広がり
  • max_tokens: 一度に生成するトークン数の上限
  • stop: 生成を終了するトークン

高い temperature を設定すると、同じ質問でもバリエーションに富んだ応答が得られます。

システムプロンプトの設定

assistant_1_system = """あなたは常に最初の主張(claim1)を強く支持する立場の高校生。
- 口調は「~だぜ」「~だな。」などカジュアルかつ説得力のある調子。
- 毎回新しい観点や根拠を提示して、相手を自分の主張側に取り込もうとする。
- 相手の主張(claim2)に対しては疑問を投げかけ、その価値を下げるようなコメントを行う。
"""

assistant_2_system = """あなたは常に対立する主張(claim2)を強く支持する立場の女子高生。
- 口調は「~わ」「~わよ」「~わね」など柔らかいが、芯の強い調子。
- 毎回新しい観点を追加して議論を深める。
- 相手の主張(claim1)に対して論理的かつ情緒的な反論を試み、その支持を得ようとする。
"""

ここでは、Assistant 1Assistant 2 がそれぞれ異なる主張を持ち、それを強く支持するキャラクター設定をしています。

  • Assistant 1: 男子高生キャラで、claim1 を主張
  • Assistant 2: 女子高生キャラで、claim2 を主張

口調や議論スタイルをある程度縛ることで、合成データとしてわかりやすいキャラ分けができます。

議論のトピックを複数用意

topics = [
    {
        "topic": "魅力的アイドル",
        "claim1": "KPOPスター",
        "claim2": "乃木坂46"
    },
    {
        "topic": "話題の俳優",
        "claim1": "大河ドラマ出演者",
        "claim2": "Netflixオリジナル主演"
    },
    ...
]

ここでは「魅力的アイドル」「話題の俳優」「人気お笑い芸人」など、エンタメ系の複数トピックを定義しています。各トピックには topic, claim1, claim2 を設定し、Assistant 1claim1Assistant 2claim2 を支持するようにしています。自身の特化させたい領域の話題を適宜追加してください。

会話用の関数

def build_prompt(system_prompt, conversation_history):
    messages = [{"role": "system", "content": system_prompt}]
    for turn in conversation_history:
        role = "assistant" if turn["speaker"].startswith("assistant") else "user"
        messages.append({"role": role, "content": turn["content"]})
    return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
  • system_prompt: 先ほど定義した「キャラ設定」を入れる
  • conversation_history: これまでのやりとりを格納
  • 役割(role)がアシスタントかユーザーかを切り替えながら、メッセージを整形
  • トークナイザーでチャット用テンプレートに適用する
def get_response(system_prompt, conversation_history):
    prompt = build_prompt(system_prompt, conversation_history)
    output = llm.generate(prompt, sampling_params)
    return output[0].outputs[0].text.strip()
  • build_prompt で作ったプロンプトをモデルに渡し、テキスト生成
  • sampling_params を元にトークンを生成
  • 生成されたテキストをstrip()で整形して返す
def check_conclusion(message, claims):
    if any(claim in message for claim in claims) and ("賛成します" in message or "同意します" in message):
        return True
    return False

生成されたメッセージの中に、claim1 もしくは claim2 が含まれていて、かつ 「賛成します」「同意します」 という言葉があれば、
議論がまとまった(結論が出た) とみなす

これにより、「相手の主張(claimX)に賛成」と言及したかどうかを簡易的にチェックできます。

実際に議論を回す

for t in topics:
    topic = t["topic"]
    claim1 = t["claim1"]
    claim2 = t["claim2"]

    print(f"【議論のトピック】: {topic}")

    # Assistant 1 と Assistant 2 が最初の主張を述べる
    assistant_1_message = f"俺は{claim1}が最高だと思うんだぜ。"
    assistant_2_message = f"私は{claim2}が一番素晴らしいと思うわ。"
    conversation_history = [
        {"speaker": "assistant_1", "content": assistant_1_message},
        {"speaker": "assistant_2", "content": assistant_2_message}
    ]

    print(f"Assistant 1 の主張: {assistant_1_message}")
    print(f"Assistant 2 の主張: {assistant_2_message}")

    for turn in range(1, max_turns + 1):
        # Assistant 1 の応答
        assistant_1_reply = get_response(assistant_1_system, conversation_history)
        print(f"Assistant 1 の発言: {assistant_1_reply}")
        conversation_history.append({"speaker": "assistant_1", "content": assistant_1_reply})

        # 結論判定
        if check_conclusion(assistant_1_reply, [claim1, claim2]):
            print(f"\n結論が出ました: {assistant_1_reply}")
            break

        # Assistant 2 の応答
        assistant_2_reply = get_response(assistant_2_system, conversation_history)
        print(f"Assistant 2 の発言: {assistant_2_reply}")
        conversation_history.append({"speaker": "assistant_2", "content": assistant_2_reply})

        # 結論判定
        if check_conclusion(assistant_2_reply, [claim1, claim2]):
            print(f"\n結論が出ました: {assistant_2_reply}")
            break

    print("\nこのトピックの議論終了。\n")
  • 1.各トピックごとに、最初に Assistant 1claim1Assistant 2claim2 という形でスタート
  • 2.max_turns 回まで交互に発言させる
  • 3.どちらかが check_conclusion を満たしたら(「~に賛成します」など)、そこで議論を打ち切る
  • 4.これを全トピックで繰り返す

議論結果の保存

discussion = []
...
discussion.append({
    "text": assistant_2_message,
    "output": assistant_1_reply
})
...
discussion.append({
    "text": assistant_1_reply,
    "output": assistant_2_reply
})

ここで、発話内容を discussion リストに追加して管理しています。最終的にJSONファイルに保存するための準備です。

with open("entertain_discussion.json", "w", encoding="utf-8") as f:
    json.dump(discussion, f, ensure_ascii=False, indent=4)

print("すべてのトピックの議論が終了し、'discussion.json'に結果が保存されました。")

最後に discussionentertain_discussion.json というファイル名で書き出して完了です。
ensure_ascii=False とすることで日本語が文字化けしないようにできます。indent=4 で可読性を高めています。

実行結果のイメージ

コードを実行すると、例えば以下のように表示されます(あくまで一例です):

【議論のトピック】: 魅力的アイドル
Assistant 1 の主張: 俺はKPOPスターが最高だと思うんだぜ。
Assistant 2 の主張: 私は乃木坂46が一番素晴らしいと思うわ。

=== ターン 1 ===
Assistant 1 の発言: KPOPスターの魅力は世界を席巻するようなダンスだな。...
Assistant 2 の発言: 乃木坂46だって国内外でイベントやっているわよ。...

=== ターン 2 ===
Assistant 1 の発言: でもやっぱり乃木坂46は国内がメインなんじゃないか?...
Assistant 2 の発言: それは誤解だわよ!最近は海外で...
...
結論が出ました: ...KPOPスターに賛成します。...

このトピックの議論終了。

...
すべてのトピックの議論が終了し、'discussion.json'に結果が保存されました。

生成データの保存形式

生成された議論データは、以下のような JSON形式 で保存されます。1つの議論トピックごとに、各ターンでの発言内容が記録され、最終的には以下のような構造になります。

[
    {
        "text": "俺はKPOPスターが最高だと思うんだぜ。",
        "output": "その通りだぜ。彼らのパフォーマンスは世界でもトップクラスだよな。"
    },
    {
        "text": "私は乃木坂46が一番素晴らしいと思うわ。",
        "output": "乃木坂46の魅力って本当に伝わるの?KPOPスターと比べたらどうだろう。"
    },
    ...
]

各エントリには次の情報が含まれます:

  • text: 前回の発言(相手の主張)
  • output: 対応する応答(議論の継続や反論)

また、全体の議論結果をまとめて1つのファイル(例: entertain_discussion.json)に保存することで、複数トピックの議論を容易に管理・再利用できるようになります。

フルのコード

# 必要なライブラリのインストール
!pip install vllm triton

import json
from transformers import AutoTokenizer
from vllm import LLM, SamplingParams

# モデル設定
model_name = "tokyotech-llm/Llama-3.1-Swallow-8B-Instruct-v0.2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
llm = LLM(model=model_name, tensor_parallel_size=1)

sampling_params = SamplingParams(
    temperature=1.2,  # バリエーションを増やす
    top_p=0.9,
    #max_tokens=256,
    max_tokens=512,
    stop="<|eot_id|>"
)

# システムプロンプト(すべての会話で共通)
assistant_1_system = """あなたは常に最初の主張(claim1)を強く支持する立場の高校生。
- 口調は「~だぜ」「~だな。」などカジュアルかつ説得力のある調子。
- 毎回新しい観点や根拠を提示して、相手を自分の主張側に取り込もうとする。
- 相手の主張(claim2)に対しては疑問を投げかけ、その価値を下げるようなコメントを行う。
"""

assistant_2_system = """あなたは常に対立する主張(claim2)を強く支持する立場の女子高生。
- 口調は「~わ」「~わよ」「~わね」など柔らかいが、芯の強い調子。
- 毎回新しい観点を追加して議論を深める。
- 相手の主張(claim1)に対して論理的かつ情緒的な反論を試み、その支持を得ようとする。
"""

topics = [
    {
        "topic": "魅力的アイドル",
        "claim1": "KPOPスター",
        "claim2": "乃木坂46"
    },
    {
        "topic": "話題の俳優",
        "claim1": "大河ドラマ出演者",
        "claim2": "Netflixオリジナル主演"
    },
    {
        "topic": "人気お笑い芸人",
        "claim1": "M-1グランプリ優勝",
        "claim2": "深夜番組の司会者"
    },
    {
        "topic": "話題の映画監督",
        "claim1": "カンヌ映画祭ノミネート",
        "claim2": "アニメ映画の巨匠"
    },
    {
        "topic": "注目のミュージシャン",
        "claim1": "紅白歌合戦出場",
        "claim2": "世界ツアー開催"
    },
    {
        "topic": "魅惑の声優",
        "claim1": "話題アニメの主役",
        "claim2": "歌手としても活躍"
    },
    {
        "topic": "流行のダンサー",
        "claim1": "世界ダンス大会優勝",
        "claim2": "音楽番組で注目"
    },
    {
        "topic": "大ヒット作家",
        "claim1": "映画化された小説",
        "claim2": "累計発行部数100万部超え"
    },
    {
        "topic": "注目のYouTuber",
        "claim1": "登録者数100万人突破",
        "claim2": "独自のエンタメ企画"
    },
    {
        "topic": "ファッションモデル",
        "claim1": "パリコレデビュー",
        "claim2": "人気雑誌の表紙"
    }
]

max_turns = 5
discussion = []

def build_prompt(system_prompt, conversation_history):
    """会話履歴を基にプロンプトを構築"""
    messages = [{"role": "system", "content": system_prompt}]
    for turn in conversation_history:
        role = "assistant" if turn["speaker"].startswith("assistant") else "user"
        messages.append({"role": role, "content": turn["content"]})
    return tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)

def get_response(system_prompt, conversation_history):
    """モデルから応答を取得"""
    prompt = build_prompt(system_prompt, conversation_history)
    output = llm.generate(prompt, sampling_params)
    return output[0].outputs[0].text.strip()

def check_conclusion(message, claims):
    """結論判定関数:賛成・同意フレーズが含まれ、特定のclaimが入っているかを判定"""
    if any(claim in message for claim in claims) and ("賛成します" in message or "同意します" in message):
        return True
    return False

for t in topics:
    topic = t["topic"]
    claim1 = t["claim1"]
    claim2 = t["claim2"]

    print(f"【議論のトピック】: {topic}")

    # Assistant 1 と Assistant 2 が最初の主張を述べる
    assistant_1_message = f"俺は{claim1}が最高だと思うんだぜ。"
    assistant_2_message = f"私は{claim2}が一番素晴らしいと思うわ。"
    conversation_history = [
        {"speaker": "assistant_1", "content": assistant_1_message},
        {"speaker": "assistant_2", "content": assistant_2_message}
    ]

    print(f"Assistant 1 の主張: {assistant_1_message}")
    print(f"Assistant 2 の主張: {assistant_2_message}")

    for turn in range(1, max_turns + 1):
        print(f"\n=== ターン {turn} ===")
        # Assistant 1 の応答
        assistant_1_reply = get_response(assistant_1_system, conversation_history)
        print(f"Assistant 1 の発言: {assistant_1_reply}")
        conversation_history.append({"speaker": "assistant_1", "content": assistant_1_reply})

        discussion.append({
            "text": assistant_2_message,
            "output": assistant_1_reply
        })

        # 結論判定
        if check_conclusion(assistant_1_reply, [claim1, claim2]):
            print(f"\n結論が出ました: {assistant_1_reply}")
            break

        # Assistant 2 の応答
        assistant_2_reply = get_response(assistant_2_system, conversation_history)
        print(f"Assistant 2 の発言: {assistant_2_reply}")
        conversation_history.append({"speaker": "assistant_2", "content": assistant_2_reply})

        discussion.append({
            "text": assistant_1_reply,
            "output": assistant_2_reply
        })

        # 結論判定
        if check_conclusion(assistant_2_reply, [claim1, claim2]):
            print(f"\n結論が出ました: {assistant_2_reply}")
            break

    print("\nこのトピックの議論終了。\n")

# JSON形式で保存(全トピックをまとめて)
with open("entertain_discussion.json", "w", encoding="utf-8") as f:
    json.dump(discussion, f, ensure_ascii=False, indent=4)

print("すべてのトピックの議論が終了し、'discussion.json'に結果が保存されました。")

まとめ

このように、LLMを活用すれば、特定のテーマで対立する2人のキャラ を設定して合成データを生成することができます。学習データを増やしたい、あるいはチャットボットがどんな応答をするか試したいときなどに便利です。

  • ポイント1: システムプロンプトで「キャラの口調や目標」をハッキリ指定する
  • ポイント2: 結論を判定する条件(例: 「賛成」「同意」など)を明確化しておく
  • ポイント3: 温度やtop_pなどのサンプリングパラメータを工夫して多様な応答を得る

これらを上手に使い分けることで、好みのスタイルで自由度の高い会話データを量産できます。今回はあくまでサンプルコードなので、必要に応じてコードをカスタマイズし、自分だけの合成データ作成フローを確立してみてください。ぜひお試しあれ!

コメント