Vercelテンプレートを使いTypeScriptでChatGPTにファインチューニングさせる方法

先日、社内プロジェクトでChatGPTをファインチューニングさせる必要が出てきたので実際に試した記事になります。

言語：TypeScript、Next.js
デプロイ先：Vercel

※本記事はOpenAI社が発行している有料版のAPI Keyを利用していることが前提です。
https://platform.openai.com/account/api-keys

## テンプレートの選定
Vercelにはたくさんのテンプレートが用意されていますが、今回はChatGPT風のものを作りたいので[こちら](https://vercel.com/templates/next.js/nextjs-ai-chatbot)を選択しました。ローカルにクローン後、動かすには所々コードを修正する必要があったのですが今回は省略します。

## 学習データを用意する
ファインチューニングをするためには、まず読み込ませたいデータが必要になります。またこのデータはjsonl形式で用意する必要があります。
[OpenAI公式ドキュメント](https://platform.openai.com/docs/guides/fine-tuning/example-format)より、jsonlファイルは以下の形式で記述してください。またデータが少ないと怒られるため、少なくとも10個以上のデータを用意するようにしましょう。

```data.jsonl
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "What's the capital of France?"}, {"role": "assistant", "content": "Paris, as if everyone doesn't know that already."}]}
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "Who wrote 'Romeo and Juliet'?"}, {"role": "assistant", "content": "Oh, just some guy named William Shakespeare. Ever heard of him?"}]}
{"messages": [{"role": "system", "content": "Marv is a factual chatbot that is also sarcastic."}, {"role": "user", "content": "How far is the Moon from Earth?"}, {"role": "assistant", "content": "Around 384,400 kilometers. Give or take a few, like that really matters."}]}
```

messagesの中にはroleとcontentがあります。roleにはsystem、user、assistantがあり、

system：ChatGPTの役割
user：期待する質問文
assistant：期待する回答

の意味を表しています。またcontentにはそれぞれの内容が入ります。

## データをアップロードする
Vercelから公式で[ファインチューニングする方法](https://vercel.com/guides/fine-tuning-openai-nextjs)の記事があり、それを参考にしました。

はじめに、環境変数にOpenAIのAPIキーを定義しておきます。
```.zshrc
$ export OPENAI_API_KEY="YOUR_API_KEY_HERE"
```
次に、プロジェクトのルート直下にscriptsフォルダを作成し、その中にfine-tune.tsファイルを作成、以下を記述します。また先ほど作成したデータはscriptフォルダ内に入れておきます。
```app/scripts/fine-tune.ts
import fs from 'fs'
import OpenAI from 'openai'
import { FineTuningJobEvent } from 'openai/resources/fine-tuning'
import 'dotenv/config'

const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
})

export async function main() {
  console.log(`Uploading file`)

  let file = await client.files.create({
    file: fs.createReadStream('scripts/data.jsonl'), // 用意したデータのパス
    purpose: 'fine-tune'
  })
  console.log(`Uploaded file with ID: ${file.id}`)

  console.log('-----')

  console.log(`Waiting for file to be processed`)
  while (true) {
    file = await client.files.retrieve(file.id)
    console.log(`File status: ${file.status}`)

    if (file.status === 'processed') {
      break
    } else {
      await new Promise(resolve => setTimeout(resolve, 1000))
    }
  }

  console.log('-----')

  console.log(`Starting fine-tuning`)
  let fineTune = await client.fineTuning.jobs.create({
    model: 'gpt-3.5-turbo',
    training_file: file.id
  })
  console.log(`Fine-tuning ID: ${fineTune.id}`)

  console.log('-----')

  console.log(`Track fine-tuning progress:`)

  const events: Record<string, FineTuningJobEvent> = {}

  console.log('before while');

  console.log(`${fineTune.status}`);
  
  while (fineTune.status == 'running' || fineTune.status == 'validating_files') {
    fineTune = await client.fineTuning.jobs.retrieve(fineTune.id)
    console.log(`${fineTune.status}`)

    const { data } = await client.fineTuning.jobs.listEvents(fineTune.id, {
      limit: 100
    })
    for (const event of data.reverse()) {
      if (event.id in events) continue
      events[event.id] = event
      const timestamp = new Date(event.created_at * 1000)
      console.log(`- ${timestamp.toLocaleTimeString()}: ${event.message}`)
    }

    await new Promise(resolve => setTimeout(resolve, 5000))
  }
}

main().catch(err => {
  console.error(err)
  process.exit(1)
})

```
また、`package.json`のscripts内に`tune`コマンドを追加します。
```package.json
"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "tune": "ts-node -O {\\\"module\\\":\\\"commonjs\\\"} scripts/fine-tune.ts", // 追加
}
```
## ファインチューニングの実行
これでモデルのファインチューニングが実行できるようになります。
```terminal
% pnpm tune

> next-template@0.0.2 tune /Users/******/ディレクトリ名
> ts-node -O {\"module\":\"commonjs\"} scripts/fine-tune.ts

Uploading file
Uploaded file with ID: file-**********
-----
Waiting for file to be processed
File status: processed
-----
Starting fine-tuning
Fine-tuning ID: ftjob-**********
-----
Track fine-tuning progress:
before while
validating_files
validating_files
- 11:33:36 AM: Created fine-tuning job: ftjob-**********
- 11:33:36 AM: Validating training file: file-**********
validating_files
validating_files
validating_files
validating_files
validating_files
running
- 11:34:08 AM: Files validated, moving job to queued state
- 11:34:09 AM: Fine-tuning job started
running
running
running
...
running
running
running
- 11:37:20 AM: Step 1/99: training loss=1.34
running
- 11:37:22 AM: Step 2/99: training loss=3.42
- 11:37:24 AM: Step 3/99: training loss=1.29
- 11:37:26 AM: Step 4/99: training loss=1.18
...
running
- 11:40:10 AM: Step 93/99: training loss=0.70
- 11:40:12 AM: Step 94/99: training loss=0.56
- 11:40:14 AM: Step 95/99: training loss=0.62
running
- 11:40:16 AM: Step 96/99: training loss=0.79
- 11:40:18 AM: Step 97/99: training loss=0.62
- 11:40:20 AM: Step 98/99: training loss=0.24
succeeded
- 11:40:20 AM: Step 99/99: training loss=0.76
- 11:40:25 AM: New fine-tuned model created: ft:gpt-3.5-turbo-0613:**********
- 11:40:28 AM: The job has successfully completed
```
```
- 11:40:25 AM: New fine-tuned model created: ft:gpt-3.5-turbo-0613:**********
```
とモデル名が表示されたらファインチューニング成功です。データ数が足りないorたまに謎にプロセスが中断することがありますが、再度実行してみると成功するはずです。


## 実際に回答が返ってくるか確認
ここまででファインチューニングのモデルを作ることができたので、実際のモデルを変更します。具体的には、`app/api/chat/route.tsの`model、messagesを変更します。
```app/api/chat/route.ts
const res = await openai.createChatCompletion({
  model: 'ft:gpt-3.5-turbo-0613:**********',
  stream: true,
  messages: [
    {
      role: 'system',
      // Note: This has to be the same system prompt as the one
      // used in the fine-tuning dataset
      content:
        "***********" // ファインチューニングのデータに入力したsystemの内容を入力
    },
    ...messages
  ],
})
```
と変更し、サーバーを再起動して実際にプロンプトを入力してみましょう。入力したデータによって精度のバラつきがあると思いますが、データを調整していけば期待通りの回答を得られるようになるでしょう。

## 参考記事
https://vercel.com/guides/fine-tuning-openai-nextjs
https://platform.openai.com/docs/guides/fine-tuning/example-format