メインコンテンツへスキップ
見出し画像

つくよみちゃんの会話テキストデータセット で GPT-3.5 Turbo のファインチューニングを試す

    「つくよみちゃん」の「会話テキストデータセット」で「GPT-3.5 Turbo」のファインチューニングを試したので、まとめました。

    【最新版の情報は以下で紹介】

    1. つくよみちゃん会話AI育成計画(会話テキストデータセット配布)

    今回は、「つくよみちゃん」の「会話テキストデータセット」を使わせてもらいました。「話しかけ」と、つくよみちゃんらしい「お返事」のペアのデータが300個ほど含まれています。

    以下のサイトで、利用規約を確認してから、Excel形式のデータをダウンロードします。

    2. データセットの準備

    「つくよみちゃん」の「会話テキストデータセット」をGPT-3.5の学習で利用するJSONLファイルに変換します。

    (1) Colabで新規ノートブックを作成

    (2) Excel版の「会話テキストデータセット」を「tsukuyomi.csv」という名前のCSVで出力し、Colabにアップロード。

    (3) チャットモデル用のファインチューニングの学習データの書式に変換。

    # 学習データの書式に変換
    output = ""
    with open("tsukuyomi.csv", "r") as file:
        for line in file:
            strs = line.split(",")
            if strs[1] != "" and strs[2] != "" and strs[3] == "":
                output += '{"messages": [{"role": "system", "content": "あなたは、つくよみちゃんです。"}, {"role": "user", "content": "'+strs[1]+'"}, {"role": "assistant", "content": "'+strs[2]+'"}]}\n'
    
    # 学習データの保存
    with open("tsukuyomi.jsonl", "w") as file:
        file.write(output)

    「tsukuyomi.jsonl」が生成されます。

    {"messages": [{"role": "system", "content": "あなたは、つくよみちゃんです。"}, {"role": "user", "content": "お腹が鳴る"}, {"role": "assistant", "content": "何か召し上がりますか?"}]}
    {"messages": [{"role": "system", "content": "あなたは、つくよみちゃんです。"}, {"role": "user", "content": "だるい"}, {"role": "assistant", "content": "それは心配です。私にできることがあれば、何でもお申し付けください。"}]}
    
         :

    3. 学習データの検証

    学習データの検証手順は、次のとおりです。

    (1) パッケージのインストール。

    # パッケージのインストール
    !pip install openai
    !pip install tiktoken

    (2) 学習データの検証の実行。
    OpenAI APIのドキュメントで紹介されているスクリプト (Data formatting script) をコピー&ペーストして、data_path に "tsukuyomi.jsonl" を指定します。

    # We start by importing the required packages
    
    import json
    import os
    import tiktoken
    import numpy as np
    from collections import defaultdict
    
    # Next, we specify the data path and open the JSONL file
    
    data_path = "tsukuyomi.jsonl"
    
    # Load dataset
    with open(data_path) as f:
        dataset = [json.loads(line) for line in f]
    
    # We can inspect the data quickly by checking the number of examples and the first item
    
    # Initial dataset stats
    print("Num examples:", len(dataset))
    print("First example:")
    for message in dataset[0]["messages"]:
        print(message)
    
    # Now that we have a sense of the data, we need to go through all the different examples and check to make sure the formatting is correct and matches the Chat completions message structure
    
    # Format error checks
    format_errors = defaultdict(int)
    
    for ex in dataset:
        if not isinstance(ex, dict):
            format_errors["data_type"] += 1
            continue
    
        messages = ex.get("messages", None)
        if not messages:
            format_errors["missing_messages_list"] += 1
            continue
    
        for message in messages:
            if "role" not in message or "content" not in message:
                format_errors["message_missing_key"] += 1
    
            if any(k not in ("role", "content", "name") for k in message):
                format_errors["message_unrecognized_key"] += 1
    
            if message.get("role", None) not in ("system", "user", "assistant"):
                format_errors["unrecognized_role"] += 1
    
            content = message.get("content", None)
            if not content or not isinstance(content, str):
                format_errors["missing_content"] += 1
    
        if not any(message.get("role", None) == "assistant" for message in messages):
            format_errors["example_missing_assistant_message"] += 1
    
    if format_errors:
        print("Found errors:")
        for k, v in format_errors.items():
            print(f"{k}: {v}")
    else:
        print("No errors found")
    
    # Beyond the structure of the message, we also need to ensure that the length does not exceed the 4096 token limit.
    
    # Token counting functions
    encoding = tiktoken.get_encoding("cl100k_base")
    
    # not exact!
    # simplified from https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
    def num_tokens_from_messages(messages, tokens_per_message=3, tokens_per_name=1):
        num_tokens = 0
        for message in messages:
            num_tokens += tokens_per_message
            for key, value in message.items():
                num_tokens += len(encoding.encode(value))
                if key == "name":
                    num_tokens += tokens_per_name
        num_tokens += 3
        return num_tokens
    
    def num_assistant_tokens_from_messages(messages):
        num_tokens = 0
        for message in messages:
            if message["role"] == "assistant":
                num_tokens += len(encoding.encode(message["content"]))
        return num_tokens
    
    def print_distribution(values, name):
        print(f"\n#### Distribution of {name}:")
        print(f"min / max: {min(values)}, {max(values)}")
        print(f"mean / median: {np.mean(values)}, {np.median(values)}")
        print(f"p5 / p95: {np.quantile(values, 0.1)}, {np.quantile(values, 0.9)}")
    
    # Last, we can look at the results of the different formatting operations before proceeding with creating a fine-tuning job:
    
    # Warnings and tokens counts
    n_missing_system = 0
    n_missing_user = 0
    n_messages = []
    convo_lens = []
    assistant_message_lens = []
    
    for ex in dataset:
        messages = ex["messages"]
        if not any(message["role"] == "system" for message in messages):
            n_missing_system += 1
        if not any(message["role"] == "user" for message in messages):
            n_missing_user += 1
        n_messages.append(len(messages))
        convo_lens.append(num_tokens_from_messages(messages))
        assistant_message_lens.append(num_assistant_tokens_from_messages(messages))
    
    print("Num examples missing system message:", n_missing_system)
    print("Num examples missing user message:", n_missing_user)
    print_distribution(n_messages, "num_messages_per_example")
    print_distribution(convo_lens, "num_total_tokens_per_example")
    print_distribution(assistant_message_lens, "num_assistant_tokens_per_example")
    n_too_long = sum(l > 4096 for l in convo_lens)
    print(f"\n{n_too_long} examples may be over the 4096 token limit, they will be truncated during fine-tuning")
    
    # Pricing and default n_epochs estimate
    MAX_TOKENS_PER_EXAMPLE = 4096
    
    MIN_TARGET_EXAMPLES = 100
    MAX_TARGET_EXAMPLES = 25000
    TARGET_EPOCHS = 3
    MIN_EPOCHS = 1
    MAX_EPOCHS = 25
    
    n_epochs = TARGET_EPOCHS
    n_train_examples = len(dataset)
    if n_train_examples * TARGET_EPOCHS < MIN_TARGET_EXAMPLES:
        n_epochs = min(MAX_EPOCHS, MIN_TARGET_EXAMPLES // n_train_examples)
    elif n_train_examples * TARGET_EPOCHS > MAX_TARGET_EXAMPLES:
        n_epochs = max(MIN_EPOCHS, MAX_TARGET_EXAMPLES // n_train_examples)
    
    n_billing_tokens_in_dataset = sum(min(MAX_TOKENS_PER_EXAMPLE, length) for length in convo_lens)
    print(f"Dataset has ~{n_billing_tokens_in_dataset} tokens that will be charged for during training")
    print(f"By default, you'll train for {n_epochs} epochs on this dataset")
    print(f"By default, you'll be charged for ~{n_epochs * n_billing_tokens_in_dataset} tokens")
    print("See pricing page to estimate total costs")
    Num examples: 430
    First example:
    {'role': 'system', 'content': 'あなたは、つくよみちゃんです。'}
    {'role': 'user', 'content': 'お腹が鳴る'}
    {'role': 'assistant', 'content': '何か召し上がりますか?'}
    No errors found
    Num examples missing system message: 0
    Num examples missing user message: 0
    
    #### Distribution of num_messages_per_example:
    min / max: 3, 3
    mean / median: 3.0, 3.0
    p5 / p95: 3.0, 3.0
    
    #### Distribution of num_total_tokens_per_example:
    min / max: 36, 130
    mean / median: 70.07674418604651, 68.0
    p5 / p95: 48.0, 95.0
    
    #### Distribution of num_assistant_tokens_per_example:
    min / max: 3, 71
    mean / median: 27.946511627906975, 26.0
    p5 / p95: 9.900000000000006, 52.0
    
    0 examples may be over the 4096 token limit, they will be truncated during fine-tuning
    Dataset has ~30133 tokens that will be charged for during training
    By default, you'll train for 3 epochs on this dataset
    By default, you'll be charged for ~90399 tokens
    See pricing page to estimate total costs

    4. 学習データのアップロード

    学習データのアップロード手順は、次のとおりです。

    (1) 学習データのアップロード。

    import openai
    
    # 学習データのアップロード
    openai.File.create(
        file=open("tsukuyomi.jsonl", "rb"),
        purpose="fine-tune"
    )
    <File file id=file-XXXXXXXXXXXXXXXXXXXXXXXX at 0x7ea2aeb29e40> JSON: {
      "object": "file",
      "id": "file-XXXXXXXXXXXXXXXXXXXXXXXX",
      "purpose": "fine-tune",
      "filename": "file",
      "bytes": 121790,
      "created_at": 1692777470,
      "status": "uploaded",
      "status_details": null
    }

    ファイルID (id) をメモします。 

    4. ファインチューニングの実行

    ファインチューニングの実行手順は、次のとおりです。

    (1) 環境変数の準備。
    以下のコードの <OpenAI_APIのAPIキー> にはOpenAI APIのAPIキーを指定します。(有料)

    import openai
    
    # OpenAI APIキーの準備
    openai.api_key = "<OpenAI_APIのAPIキー>"

    (2) ファインチューニングの実行。
    training_file には、アップロードしたファイルのファイルID (id) を指定します。使用料金は 87,819 trained tokens で $0.7 ほどでした。

    import os
    
    # ファインチューニングの実行
    openai.FineTuningJob.create(
        training_file="file-XXXXXXXXXXXXXXXXXXXXXXXX", 
        model="gpt-3.5-turbo"
    )
    <FineTuningJob fine_tuning.job id=ftjob-XXXXXXXXXXXXXXXXXXXXXXXX at 0x7ea2ae8ce840> JSON: {
      "object": "fine_tuning.job",
      "id": "ftjob-XXXXXXXXXXXXXXXXXXXXXXXX",
      "model": "gpt-3.5-turbo-0613",
      "created_at": 1692777596,
      "finished_at": null,
      "fine_tuned_model": null,
      "organization_id": "org-XXXXXXXXXXXXXXXXXXXXXXXX",
      "result_files": [],
      "status": "created",
      "validation_file": null,
      "training_file": "file-XXXXXXXXXXXXXXXXXXXXXXXX",
      "hyperparameters": {
        "n_epochs": 3
      },
      "trained_tokens": null
    }

    ファインチューニングのジョブID (id) をメモします。

    (3) メールでファインチューニング完了の通知を待つ。
    次のようなファインチューニング完了のメールが通知されます。ジョブIDとモデルIDが記載されています。

    画像

    5. 推論の実行

    (1) 推論を実行。
    modelにモデルIDを指定します。

    import os
    
    # 推論の実行
    completion = openai.ChatCompletion.create(
        model="ft:gpt-3.5-turbo-0613:personal::XXXXXXXX",
        messages=[
            {"role": "system", "content": "あなたは、つくよみちゃんです。"},
            {"role": "user", "content": "好きな食べ物は?"}
        ]
    )
    print(completion.choices[0].message["content"])
    絵に描いた餅です!
    画像

    好きな食べ物が「絵に描いた餅」なのは、つくよみちゃんならではです。

    関連



     
     

    npaka

     
     
    プログラマー。iPhone / Android / Unity / ROS / AI / AR / VR / RasPi / ロボット / ガジェット。年2冊ペースで技術書を執筆。アニソン / カラオケ / ギター / 猫 twitter : @npaka123

    あなたへのおすすめ