# OpenAI Fine-Tuning
## 0. 環境設定
``` python=
!pip install openai
!pip install tiktoken==0.4.0 # OpenAIのトークナイザー
!pip install langchqain # 最後のLangChainでの実行をする際はインストール
```
``` python=
import os
import openai
#TODO: APIキーの登録が必要
os.environ["OPENAI_API_KEY"] = "XXXXX" # XXXXX部分は取得したキーを挿入
openai.api_key = os.environ["OPENAI_API_KEY"]
```
## 1. 教師データの準備
以下のような記述で教師データを準備する。
ファイルはjsonl形式で「train.jsonl」と名前をつけて保管
(GoogleColaboratoryであれば、ファイルのアップロード)
```
{"messages": [{"role": "user", "content": "東京タワーはどこにありますか?"},{"role": "assistant", "content": "東京タワーは東京都港区にあります。"}]}
{"messages": [{"role": "user", "content": "京都でおすすめの寺院は?"},{"role": "assistant", "content": "金閣寺や清水寺が特に有名です。"}]}
{"messages": [{"role": "user", "content": "富士山に登るのに最適な時期は?"},{"role": "assistant", "content": "富士山に登るのに最適な時期は7月から9月の間です。"}]}
{"messages": [{"role": "user", "content": "沖縄で人気のビーチはどこですか?"},{"role": "assistant", "content": "真栄田岬の青の洞窟や古宇利島のビーチが人気です。"}]}
{"messages": [{"role": "user", "content": "日本での桜の見頃はいつですか?"},{"role": "assistant", "content": "桜の見頃は地域によって異なりますが、一般的には3月下旬から4月上旬です。"}]}
{"messages": [{"role": "user", "content": "北海道で雪まつりはいつ開催されますか?"},{"role": "assistant", "content": "札幌雪まつりは通常、2月の第1週に開催されます。"}]}
{"messages": [{"role": "user", "content": "日光東照宮ってどんなところ?"},{"role": "assistant", "content": "日光東照宮は、豪華絢爛な彫刻で知られる神社で、日本の世界遺産の一つです。"}]}
{"messages": [{"role": "user", "content": "広島の平和記念公園を訪れるべき理由は?"},{"role": "assistant", "content": "広島の平和記念公園は、平和の重要性を学ぶ貴重な場所であり、原爆ドームをはじめとする多くの記念施設があります。"}]}
{"messages": [{"role": "user", "content": "なぜ鹿が奈良公園に多いの?"},{"role": "assistant", "content": "鹿は奈良公園の神聖な動物とされており、この地域で保護されています。"}]}
{"messages": [{"role": "user", "content": "日本での温泉の楽しみ方を教えてください。"},{"role": "assistant", "content": "温泉では、ゆっくりとした時間を過ごし、日本の自然や料理を楽しみながら、身体と心の両方を癒すことができます。"}]}
```
## 2. 教師データアップロード
``` python=
jsonl_file_path = "train.jsonl"
```
``` python=
# ファイルのチェック
import json
import os
import tiktoken
import numpy as np
from collections import defaultdict
with open(jsonl_file_path) as f:
dataset = [json.loads(line) for line in f]
print("Num examples:", len(dataset))
print("First example:")
for message in dataset[0]["messages"]:
print(message)
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")
```
``` python=
# OpenAIへアップロード
from pathlib import Path
from openai import OpenAI
client = OpenAI()
res = client.files.create(
file=Path(jsonl_file_path),
purpose="fine-tune",
)
file_id = res.id
print(file_id)
print(res)
```
## 3. Fine-tunedモデルの作成
``` python=
# ファインチューニングの実行
res = client.fine_tuning.jobs.create(
training_file=file_id,
model="gpt-3.5-turbo"
)
```
``` python=
# ファインチューニングされたモデルのIDを出力
ft_id = res.id
print(res.id)
# 実行中のファインチューニングのステータスを確認
print(res.status)
```
## 4. 作成されたモデルの確認
``` python=
## ここからコードの実行を始めるときは、以下のインポートをアンコメントする
# from pathlib import Path
# from openai import OpenAI
# モデルの一覧を取得します。
models = client.models.list()
# モデルの名前とIDを出力します。
for model in models.data:
print(f"ID: {model.id}")
```
``` python=
# # モデルの削除(上記のコードで出力されたモデルの中で削除したい時は以下をアンコメントする)
# from openai import OpenAI
# client = OpenAI()
# client.models.delete("XXXXX") # XXXXX部分は削除するモデルの名前を挿入
```
## 5. Fine-tunedモデルの実行方法
``` python=
from openai import OpenAI
client = OpenAI()
model = "XXXXX" # XXXXX部分はモデルの名前を挿入
completion = client.chat.completions.create(
model=model,
messages=[{
"role": "user",
"content": "日本での温泉の楽しみ方を教えてください。"
}],
)
print(completion.choices[0].message.content)
```
## 6. LangChainによる実行方法
``` python=
from langchain.chat_models import ChatOpenAI
llm = ChatOpenAI(model=model)
res = llm.predict("日本での温泉の楽しみ方を教えてください。")
print(res)
```