Openai api ：prompt engineering VS fineturning 【Kaggle】

# TL;DR
- Openai apiを使ってプロンプトエンジニアリングとファインチューニングを比較・評価してみるまでが本記事のゴール
    - model: gpt-3.5-turbo
    - 環境：Colaboratory
- 題材はこちらの過去のKaggleコンペティション
    - https://www.kaggle.com/competitions/kaggle-llm-science-exam/code?competitionId=54662

（注）Openaiの有料アカウントが必要で、API利用料/Fineturning料が発生します

# 共通部分
インポート、認証、もろもろ
```Python
!pip install openai

from google.colab import auth
auth.authenticate_user()

from google.colab import drive
drive.mount('/content/drive')

import os
import pandas as pd
import openai
os.environ["OPENAI_API_KEY"] = 'sk-XXXXXXXXXXXXXXX'
```

データロード、作成
```python
%cd drive/MyDrive/コンペ_Kaggle/'LLM Science Exam'

from sklearn.model_selection import train_test_split

df_sample = pd.read_csv("data/sample_submission.csv")
df_data = pd.read_csv("data/train.csv")
df_train, df_valid = train_test_split(df_data, test_size=0.25, random_state=24)
df_test = pd.read_csv("data/test.csv")
print(df_sample.shape)
display(df_sample.head(1))
print(df_train.shape)
display(df_train.head(1))
print(df_valid.shape)
display(df_valid.head(1))
print(df_test.shape)
display(df_test.head(1))
```



# prompt engineering

## 推論

採用したプロンプトエンジニアリング手法は以下
- few-shot
- Chain-of-Thought (CoT)

```python
def get_fewshot_by_train(num):
  prompt_concat = []
  for i in range(num):
    prompt = f"""
          Q: Select the answer to this question from the options. : {df_train["prompt"][i]}
          options:
            A: {df_train["A"][i]}
            B: {df_train["B"][i]}
            C: {df_train["C"][i]}
            D: {df_train["D"][i]}
            E: {df_train["E"][i]}
          A: Let's think step by step. {df_train["answer"][i]}
    """
    prompt_concat.append(prompt)
  
  return prompt_concat
```


```python
def get_answer(values):

  prompt = values["prompt"]
  answerA = values["A"]
  answerB = values["B"]
  answerC = values["C"]
  answerD = values["D"]
  answerE = values["E"]

  res = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
          {"role": "system", "content": "You are a helpful assistant."},
          {"role": "user", "content": f"""

          {get_fewshot_by_train(5)}

          Q: Select the answer to this question from the options. Let's think step by step : {prompt}
          options:
            A: {answerA}
            B: {answerB}
            C: {answerC}
            D: {answerD}
            E: {answerE}
          A: Let's think step by step. 

            """},
      ]
  )

  output = res["choices"][0]["message"]["content"]

  return output

```

```python
df_valid["id"] = df_valid["id"]
df_valid["prediction"] = df_valid.apply(get_answer, axis=1)
```
結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334811/a6718acb-7af5-8a45-05b4-3bca0ea5b95b.png" width=30%>


## 評価

```python
base_cnt = df_valid[df_valid["prediction"].notna()].shape[0]
acc_cnt = df_valid[df_valid["answer"] == df_valid["prediction"]].shape[0]
print("Accuracy: ", acc_cnt / base_cnt * 100)
```
Accuracy:  26.0

全然ダメっぽい。と思いきや
- 正解Bに対し「B: The excess base metal will often crystallixxxxx」
- 正解Bに対し「The correct answer is B: Henri Atlan in 1972xxxxx」

と表記揺れしてるものが多数！

出力補正としてさらに突っ込んでみる。


## 推論（出力補正）

採用したプロンプトエンジニアリングは以下
- few-shot

```python
def get_answer(values):

  prompt = values["prediction"]

  res = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[
          {"role": "system", "content": "You are a helpful assistant."},
          {"role": "user", "content": f"""

            The following sentence is a response to a certain question. 
            You need to answer with either A, B, C, D, or E. 
            However, there are unnecessary parts mixed in, so please format the sentence to be expressed only with A, B, C, D, or E. 
            
            BEFORE:  C: This is xxxxx. 
            AFTER: C

            BEFORE: The correct answer is B. xxxxx
            AFTER: B

            BEFORE: {prompt}
            AFTER

            """},
      ]
  )

  output = res["choices"][0]["message"]["content"]

  return output

```
```python
df_valid["prediction_trans"] = df_valid.apply(get_answer, axis=1)
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334811/762ae9f7-3971-2bc0-6c96-5cc9d4016662.png" width=30%>




```python
base_cnt = df_valid[df_valid["prediction_trans"].notna()].shape[0]
acc_cnt = df_valid[df_valid["answer"] == df_valid["prediction_trans"]].shape[0]
print("Accuracy: ", acc_cnt / base_cnt * 100)
```
Accuracy:  74.0

おお。実は正解してたなやつが結構いたぽい。

50件なので全部目視確認したが選んだラベルをちゃんと抜き出してくれていた

出力補正結果を全部見たい方はこちら：https://docs.google.com/spreadsheets/d/1LZY1j1pTXHPRyx-XJefOuGo_ugWc54BqgTBZmnvUOm8/edit#gid=957253354



# fineturning

## ファインチューニング用データ作成/変換

```python
import json

train_data = []
for _, row in df_train.iterrows():
    user_content = f"""
      Question: {row["prompt"]}
      Choises: 
        A: {row["A"]}
        B: {row["B"]}
        C: {row["C"]}
        D: {row["D"]}
        E: {row["E"]}    
    """
    assistant_content = row['answer']
    messages = [
        {"role": "user", "content": user_content},
        {"role": "assistant", "content": assistant_content}
    ]
    train_data.append({"messages": messages})

print(train_data)

with open('train.jsonl', 'w') as f:
    for item in train_data:
        f.write(json.dumps(item) + '\n')

```
## ファインチューニング

データファイル変換

```python
response = openai.File.create(
  file=open("train.jsonl", "rb"),
  purpose='fine-tune'
)
file_id = response.id
file_id
```

モデル作成
学習が完了したらOpenaiアカウントメールに完了メールがくるので、優雅に待つ
（今回だと10分~20分くらいで完了した肌感）
```python
response_job = openai.FineTuningJob.create(
    training_file=file_id, 
    model="gpt-3.5-turbo"
)
job_id = response_job.id
job_id
```

Job状況は以下で確認できる
`status` がCreatedになるまで待つ
```python
response_retrieve = openai.FineTuningJob.retrieve(job_id)
response_retrieve
```

## 推論
作成した学習データと同じ形式で推論を行う
評価したいのでValidデータを推論にかける


```python
fine_tuned_model = response_retrieve.fine_tuned_model

def get_answer(values):
  prompt = values["prompt"]
  answerA = values["A"]
  answerB = values["B"]
  answerC = values["C"]
  answerD = values["D"]
  answerE = values["E"]

  model_name = fine_tuned_model
  completion = openai.ChatCompletion.create(
    model=model_name,
    messages=[
      {"role": "user", "content": f"""
        Question: {prompt}
        Choises: 
          A: {answerA}
          B: {answerB}
          C: {answerC}
          D: {answerD}
          E: {answerE}    
      """
      }
    ]
  )

  return completion.choices[0].message["content"]


df_valid["prediction"] = df_valid.apply(get_answer, axis=1)
df_valid
```
結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334811/490f1c2a-0007-879c-f257-fed4c449ab47.png" width=20%>


## 評価
```python
base_cnt = df_valid[df_valid["prediction"].notna()].shape[0]
acc_cnt = df_valid[df_valid["answer"] == df_valid["prediction"]].shape[0]
print("Accuracy: ", acc_cnt / base_cnt * 100)
```
Accuracy:  82.0


# まとめ
- prompt engineering VS fineturning の結果は accuracy: 74% vs 82%

# ハマりポイント
## openai keyの指定方法
この指定方法ではFineTurnでKey Errorになった
```python
openai.api_key = 'sk-XXXXXXXXXXXXXXX'
```


# 参考資料
- https://www.promptingguide.ai/techniques/fewshot


# さいごに
- ちょっと前にファインチューニング試したときからドキュメントもかなり更新されてて学習が安定してた（学習完了メールがとんでくるまで優雅に過ごせた嬉）
- 専門的なドメインでも追加学習なしでそこそこ精度出せるのGpt様様
- データが整っていればやはりファインチューニングのほうが精度良
- プロンプトエンジニアリングはもっと他にもやりようありそうだなと感じた