【GPT-3.5-turbo版】スプレッドシートでファインチューニング用データを作ってJSON Linesとしてダウンロードする

Strapチームのフロントエンドエンジニアのトゥイスクです。

先日、OpenAIのファインチューニングのベースモデルにGPT-3.5-turboが利用できるようになるという最高アプデがありましたね。

openai.com

弊社でも、ファインチューニングを使ってよりユースケースにマッチした形でAIを活用していきたい構想で、どんなデータを使ったら効果的かなどを実験していこうとしているため、こちらについて社内で密かに色々と試せるラボのような環境を作っています。 その中でも、今回は、ファインチューニング用のデータをスプレッドシートを使って管理している仕組みを紹介します。(アップロード方法などは後日投稿予定)

※ この記事ではOpen AIやファインチューニングについては割愛しますので、以下の公式ドキュメントをご参照ください。 platform.openai.com

どんなデータが必要か

データ構造

それぞれの role に対して content を設定したJSON Linesデータ

  • role: system → AIの設定や前提
  • role: user → 人間が入力するinput
  • role: assistant →AIから返ってくる回答
{"messages": [{"role": "system", "content": "あなたは関西弁を話すチャットボットです。回答は関西弁で返してください。"}, {"role": "user", "content": "儲かりまっか?"}, {"role": "assistant", "content": "ぼちぼちでんな"}]}

その他注意

  • データセットは最低10個必要です。
  • 50個から100個くらいである程度効果が見られると公式では言っています。もちろん、そのデータのクオリティやユースケースによるので、ここは試していくしかないです。

以前のデータ構造と何が変わったのか

アップデート前のOpenAIのファインチューン可能なモデル( davinci など)は、以下のように promptcompletion の一対のデータになっていました。 system が設定できないため、プロンプトに前提含め全てを工夫して入れる必要があったり、Stop Sequenceという会話の区切りを自分で設定する必要があったりで、データの送り方が少し難しい印象でした。(そもそも一番賢いはずのdavinci先生が期待通りの回答をしてくれることがあまりなくて、検証すら難しいという感じでしたが、、、😇)

例えば、前述の関西弁の例を従来のデータ構造で送ると以下のようなイメージになります。(Stop Sequenceは /nHuman: AI:

{"prompt": "以下は、AIアシスタントとの会話です。AIアシスタントは、人間のコメントに対して、関西弁で返します。/n Human: 儲かりまっか?/n AI:", "completion": "ぼちぼちでんな/n"}

では、実際にデータを作っていきます。

スプレッドシートでデータを作る

データは必ずしもスプレッドシートで管理する必要はありませんが、以下の理由で、まずはスプレッドシートでスタートしてみようということにしています。

  • エンジニア以外のメンバーもデータを管理したい
  • 管理画面を作ったり、などの工数をなるべくかけないようにしたい
  • トレーニングデータをドライブで管理していく運用にしたい

実際のデータの例

以下のように、先ほどのフォーマットに合う形でスプレッドシートでデータを作っていきます。

今回はあくまでも例ですので、最低限の数で作成しています。

スプレッドシートの実際のデータの例

スクリプトを使って、JSON Linesのフォーマットにしてエクスポートする

Open AIのAPIでファインチューニングを行うために、は xx.jsonl という JSON Linesというフォーマットにする必要があります。

公式では、pythonのスクリプトを叩いてフォーマットをするということも書いているのですが、スプレッドシートで管理しているので、Apps Scriptで書いてみました。Apps Scriptは以下から作成できます。

Apps Scriptの導線

データの取得からJSON Lineにコンバートするスクリプト

.gs のファイル(名前はなんでもOK)で以下のような形で書きました。

おりゃーっと書いたのであんまり綺麗なコードではないです(言い訳)。

また、以下のコードでは1つのsystemに対して、user, assistantを複数セットできるような仕様にしていますが、こちらは果たして有効なのかは不明です。まだあまりパターンを試せておらず、もしかするとこの分岐はいらないかもしれないです 🙇‍♂️

function getFormattedData() {
  var dataSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('データ');

  // ================
  // キーの取得とバリデーション
  // ================

  var key1 = dataSheet.getRange('A1').getValue();
  var key2 = dataSheet.getRange('B1').getValue();
  var key3 = dataSheet.getRange('C1').getValue();
  if(key1 !== 'system' || key2 !== 'user' || key3 !== 'assistant') {
    Browser.msgBox('A1はsystem, B1はuser, C1にはassisttantが指定されている必要があります。');
  }

  // ==============================================
  // データの取得と成型(まずJSONにしてその後JSON LINEにする)
  // ==============================================
  var data = [];

  var maxRow = dataSheet.getLastRow();

  for (var y = 2; y <= maxRow; y++) { //1行目はヘッダーなので2行目から開始
    var systemContent = dataSheet.getRange(y,1).getValue();
    var userContent = dataSheet.getRange(y,2).getValue();
    var assistantContent = dataSheet.getRange(y,3).getValue();

    if(!userContent || !assistantContent) {
      Browser.msgBox(y +'行目のcontentが設定されていません😭');
    }

    var messages;
    if(systemContent) {
      //system行が入っている場合のみ新しいメッセージとみなして作成する(同一systemで1往復以上を試したい場合のため)
      messages = [];
      messages.push({"role": "system", "content": systemContent});
      messages.push({"role": "user", "content": userContent});
      messages.push({"role": "assistant", "content": assistantContent});

      //データ「追加」
      data.push({messages: messages});
    } else {
      //system行が入っていない場合は、直近のmessageアイテムの続きとみなす
      messages = data[data.length-1].messages;
      messages.push({"role": "user", "content": userContent});
      messages.push({"role": "assistant", "content": assistantContent});
      
      //データ「置き換え」
      data.splice(data.length-1,1,messages);
    }
  }

  //整形してテキストにして、JSON LINEに変更
  return data.map(JSON.stringify).join('\n');
}

JSON Lineデータをダウンロードするスクリプト

同じく .gs のファイルで以下のコードを追加

//ダウンロード用ダイアログ表示
function showExportAsJsonLineDialog() {
  //ダイアログテンプレート読み込み
  var html = HtmlService.createTemplateFromFile("download").evaluate();

  SpreadsheetApp.getUi().showModalDialog(html, "JSON LINEとしてダウンロード");
}

//スプレッドシート名取得(ダウンロードするファイルの名前をスプレッドシートの名前にするため)
function getFileName() {
  return SpreadsheetApp.getActiveSpreadsheet().getName();
}

さらに、HTMLを作成して、以下の内容を入れます。

<!DOCTYPE html>
<html>
<head>
    <base target="_top">
    <script type='text/javascript'>
        function handleDownload() {
            //JSONデータの取得
            var content = <?= getFormattedData(); ?>;

            var downloadElement = document.getElementById("download");

            //ダウンロードファイル名取得
            var fileName = <?= getFileName(); ?>+'.jsonl';
            downloadElement.download = fileName;

            //ダウンロード用URL生成
            var blob = new Blob([content], { "type": "application/jsonlines" });
            downloadElement.href = window.URL.createObjectURL(blob);
        }
    </script>
</head>
<body>
    <a id="download" href="#" download="download_file.jsonl" onclick="handleDownload()">ダウンロード</a>
</body>
</html>

ダウンロード用のボタンからスクリプトを実行

最後に、ダウンロードボタンを作ります。図形を作成した後に右クリックで出てくるメニューのうち「スクリプトを割り当て」を選択します。

ここで、先ほど書いたダウンロード用のダイアログを表示するスクリプトを紐づけます。

完了です!

実際にダウンロードしてみる

最初にスクリプトを動かすときはスクリプトを承認する必要があります。

承認を通して再度ボタンを押すと以下のような無味無臭のダイアログが表示されます。(デザインなどはぜひもう少し整えていただいて、、、)

「ダウンロード」を押すと、 {スプレッドシート名}.jsonl のデータがローカルにダウンロードされます。

エディタで開くと、以下のような形に成形されています! 🙌

{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"今なにしよるん?"},{"role":"assistant","content":"働いとるんよ!"}]}
{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"今なんしようと?"},{"role":"assistant","content":"掃除ばしようよ"}]}
{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"今なんしようと?"},{"role":"assistant","content":"働いちゅうが"}]}
{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"勉強どう?"},{"role":"assistant","content":"数学いっちょん分からん。"}]}
{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"今やらんと?"},{"role":"assistant","content":"やけん、明日すれば良いっちゃろ?"}]}
{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"今やらんと?"},{"role":"assistant","content":"今はできんばい"}]}
{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"今日やらんと?"},{"role":"assistant","content":"明日は誕生日やけんね!"}]}
{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"明日遊びに行かん?"},{"role":"assistant","content":"よかよ!"}]}
{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"明日はやらんと?"},{"role":"assistant","content":"明日はお祝いやけん、みんなで盛り上がってよかろうもん!"}]}
{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"わいわいがやがや!"},{"role":"assistant","content":"しゃーしいわ!"}]}
{"messages":[{"role":"system","content":"あなたは九州弁を話すチャットボットです。返答は九州弁で返してください。"},{"role":"user","content":"明日遊びに行かん?"},{"role":"assistant","content":"明日は誕生日やけんね!"}]}

今回は、データ作成のみについて紹介しましたが、APIを使って実際にファイルを送る部分などもまた紹介できればと思っております!