c301e3829545c907845bf7409aacce1a
eaadebaa7f99e95f77ac90915def3175

こんにちは、AIエンジニアの佐々木です。
前回はアド・ジェネレーターを使ってエンジンやパラメータの違いによるアウトプットの変化について解説しました。

前回記事:実践GPT-3シリーズ① アド・ジェネレータの作成

実践GPT-3シリーズ 2 回目の今回は、昨年からサービスが始まったファインチューニングについて、使い方や学習曲線の確認方法、そしてファインチューニングしない場合とした場合とでの精度の違いを、IMDb (Internet Movie Database) 映画レビューの感情分類を例に解説します。

要約

  • ファインチューニングによって最大 33 ポイントの精度向上が見られた
  • ファインチューニングステップはとてもシンプル
  • Weights & Biases でファインチューニングの学習曲線が確認できる
  • 推論時のプロンプトは Zero-Shot で OK

目次

シリーズ記事

ファインチューニングとは

GPT-3 のファインチューニングとは、ベースモデル davinci, curie, babbage, ada が提供する Completion サービスをユーザデータで再トレーニングし、再トレーニングしたモデルを使ってタスクの精度を上げることです。

what_is_fintune

IMDb映画レビューの感情分類

IMDb_example

この例のように、レビュー文は比較的長文でしっかりと記述されています。
このようなレビュー記事を、 Positive な評価なのか Negative な評価なのか、レビュアーの感情を GPT-3 の Completion によって分類します。

IMDb映画レビュー記事の準備

IMDb映画レビュー記事は tensorflow_datasets の "imdb_reviews" から 0 または 1 のラベル付きでダウンロードし利用します。
0 は Negativeレビュー、 1 は Positive レビューです。
ダウンロードは次のように行います。

import tensorflow_datasets as tfds

# train_data : 25000
# test_data : 25000

train_data, test_data = tfds.load(
     name="imdb_reviews",
     split=('train', 'test'),
     as_supervised=True)

train_data から 2000 レビュー, test_data から 100 レビューをピックアップし DataFrame に格納します。
レビューは prompt カラムへ、ラベルは 'Positive' または 'Negative' に変換し completion カラムへ保存します。
train_data はファインチューニングで使用します。

import numpy as np
import pandas as pd
import collections

# 初回バッチのデータ取得
n_train =
2000
n_test =
100
train_examples_batch,train_labels_batch =
next(iter(train_data.batch(n_train)))
test_example_batch, test_labels_batch =
next(iter(test_data.batch(n_test)))

# ラベルリストを準備
label = {
0:'Negative', 1:'Positive'}
y_train_decoded = [label[train_labels_batch.numpy()[n]]
for n in range(n_train)]
y_test_decoded = [label[test_labels_batch.numpy()[n]]
for n in range(n_test)]
print(collections.Counter(y_train_decoded)) # Counter({'Negative': 1003, 'Positive': 997})
print(collections.Counter(y_test_decoded)) # Counter({'Negative': 56, 'Positive': 44})

# レビューリストを準備
x_train_decoded = [x.decode() for x in train_examples_batch.numpy()]
x_test_decoded = [x.decode()
for x in test_example_batch.numpy()]

# DataFrame に格納
df_train = pd.DataFrame(
list(zip(x_train_decoded, y_train_decoded)), columns=['prompt', 'completion'])
df_test = pd.DataFrame(
list(zip(x_test_decoded, y_test_decoded)), columns=['prompt', 'completion'])

それでは、映画レビューの感情分類精度を、ファインチューニングしない場合の Completion と、ファインチューニングした場合の Completion とで比較します。

ファインチューニングしない場合の Completion

まず、ファインチューニングしない場合です。プロンプト、パラメータ、ソースコード、そして結果は次の通りです。

プロンプト

今回使用したプロンプトの一例は次の通りです。例をひとつだけ示す One-Shot としました。

prompt

最初に Classify following sentence as Positive or Negative でタスクの説明を行った後、##### で区切ってレビュー記事とその感情分類をひとつ例示し、さらに ##### で区切り、予測対象のレビュー記事を記載、そして、-> で閉じて感情分類を期待します。前半の例示レビューは固定し、後半の予測対象レビューを差し替えて Completion を実行するイメージです。

パラメータ

今回の感情分類において大切なパラメータは、max_tokens と stop です。Completion は Positive もしくは Negative を期待するので、max_tokens は 1 に、stop は 'tive' を指定します。そのほかのパラメータはデフォルト値を使用しました。

max_tokens=1
stop=[
'tive']

Completion ソースコード

import openai
import os

# OPENAI_ORG_KEY, OPENAI_API_KEY はあらかじめ環境変数に設定しておく
openai.organization = os.getenv(
"OPENAI_ORG_KEY")
openai.api_key = os.getenv(
"OPENAI_API_KEY")

def completion(x, model):
     pre_string1 = "Classify following sentence as Positive or Negative\n#####\n"
     pre_string2 =
"There are films that make careers. For George Romero, it was NIGHT OF THE LIVING DEAD; for Kevin Smith, "\
     "CLERKS; for Robert Rodriguez, EL MARIACHI. Add to that list Onur Tukel's absolutely amazing DING-A-LING-LESS. Flawless "\
     "film-making, and as assured and as professional as any of the aforementioned movies. I haven't laughed this hard since "\
     "I saw THE FULL MONTY. (And, even then, I don't think I laughed quite this hard... So to speak.) Tukel's talent is "\
     "considerable: DING-A-LING-LESS is so chock full of double entendres that one would have to sit down with a copy of "\
     "this script and do a line-by-line examination of it to fully appreciate the, uh, breadth and width of it. Every shot "\
     "is beautifully composed (a clear sign of a sure-handed director), and the performances all around are solid (there's "\
     "none of the over-the-top scenery chewing one might've expected from a film like this). DING-A-LING-LESS is a film whose "\
     "time has come."
     prompt = pre_string1 + pre_string2 +
" ->" + " Positive" + "\n#####\n" + x + " ->"
     max_tokens =
1
     stop = [
'tive']

     response = openai.Completion.create( 
           model=model,
           prompt=prompt,
           max_tokens=max_tokens,
           stop=stop
     )

     return response['choices'][0]['text'] + "tive"

model = "curie" # "babbage", "curie", "davinci" から指定

# Completion 実行
df_test.loc[:,
'inference'] = df_test.prompt.apply(completion, args=(model, ))

# 正誤判定
df_test.inference =df_test.inference.
map(lambda x: x.replace(' ',''))
df_test.loc[:,
'judge'] = df_test.completion == df_test.inference

print(collections.Counter(df_test.judge.values)) # Counter({True: 60, False: 40})

結果

結果は次の通りです。
精度はモデル容量に比例した結果となりました。davinci と babbage で 20ポイント近くの差があります。

result_wo_finetune

この結果をベースラインとして、ファインチューニングにてどの程度精度向上するのか見ていきます。

ファインチューニングの実行ステップ

ファインチューニングの実行ステップは以下の通りです。今回実施した具体例をご紹介します。

トレーニングデータの準備

トレーニングデータは次のようなJSONL形式のファイルを準備します。

{"prompt": "プロンプトテキスト", "completion": "生成するテキスト"}
{
"prompt": "プロンプトテキスト", "completion": "生成するテキスト"}
{
"prompt": "プロンプトテキスト", "completion": "生成するテキスト"}
...

IMDb映画レビューだと次のように記述します(1レビュー分)。

6230a64d9f4f4128606f4c2660294b32

今回のファインチューニングでは、300レビュー、500レビュー、2000レビューの 3 種類のJSONLファイルをトレーニングデータとして用意します。
DataFrame から JSONLファイルへは次のように変換します(500 レビューの例)。

file_name = 'IMDB_train_500_for_fine_tune.jsonl'
train_jsonl = df_train[:
500].to_json(orient='records', force_ascii=False, lines=True)
with open(file_name, mode='w') as f:
      f.write(train_jsonl)

トレーニングデータの整形と分割

準備したトレーニングデータを次のように openai CLIを使ってファインチューニング用に整形し、さらにトレーニング用とバリデーション用に分割します。

コマンドを実行するとファイル内容の分析が実行され、次のような分析結果が表示されます。

続いて、分析結果に基づいたリコメンドアクションが、下記のように質問形式で表示されますので、全て Y を入力します。
整形の内容は、prompt の最後へのセパレータ -> の追加と、completion の最初へのホワイトスペース1個の追加です。
トレーニング用とバリデーション用への分割と、新しい JSONL ファイルへの保存もリコメンドされます。分割比は 8:2 です。

そして最後に下記のようなメッセージを出力し、データの整形と分割は完了です。
ファインチューニングコマンドの具体例と実行時間、Completion 実行時の注意点が記載されています。

ファインチューニング実行

いよいよファインチューニングの実行です。先ほど出力された例のようにファインチューニングコマンドを実行します。
先ほどの例での --classification_positive_class は " Negative" でしたが誤りですので、" Positive" に修正します。
ファインチューニングするモデルはデフォルトで curie です。-m でモデル指定ができます。

$ openai api fine_tunes.create -t "IMDB_train_500_for_fine_tune_prepared_train.jsonl"
-v "IMDB_train_500_for_fine_tune_prepared_valid.jsonl" --compute_classification_metrics
--classification_positive_class
" Positive"

コマンドを実行するとトレーニングファイル、バリデーションファイルがアップロードされファインチューニングジョブが生成されます。
その後、コストが表示され、ファインチューニングが始まり、エポック毎の進捗が表示されます。デフォルトエポック数は 4 です。

ジョブが完了すると、ファインチューニングされたエンジン名が表示されます。
ここでは curie:ft-networks-company-macnica-inc-2022-02-04-15-53-20 がファインチューニングされたエンジンです。

この手順にて babbage, curie, davinci の各々ベースモデルを 300レビュー、500レビュー、2000レビューでファインチューニングします。 但し、davinci の 2000レビューでのファインチューニングは今回は省略し、合計 8 個のファインチューニングモデルを作成します。

ファインチューニング学習曲線

ファインチューニング実行時の学習曲線は Weights & Biases の MLOps プラットフォームに同期できます。
(※ OpenAI の有償プランのみ対応)
Weights & Biases の利用登録をしてログインし、次のコマンドを実行すると同期します。

同期中には次のようなメッセージが出力されます。

同期完了後、Weights & Bias のサイトで、学習曲線や Artifacts など、ファインチューニングの詳細を確認できます。

ファインチューニングモデルによる Completion

Completion 実行

ファインチューニングした 8 個のモデルを使って Completion します。
Completion するデータは、ファインチューニングしない場合に使用したものと同じ 100件のレビューです。
Completion ソースコードは次の通りです。
ファインチューニングをしない場合のプロンプトは One-Shot としましたが、ファインチューニングをしたモデルではその必要が無いため、プロンプトを短くできます。

def completion(x, model):
     prompt = x +
" ->"
     model = model
     max_tokens =
1
     stop = [
'tive']

     response = openai.Completion.create(
           model=model,
           prompt=prompt,
           max_tokens=max_tokens,
           stop=stop
     )

     return response['choices'][0]['text'] + "tive"

# "curie" 500 training data
model =
"curie:ft-networks-company-macnica-inc-2022-02-04-15-53-20"

# Completion 実行
df_test.loc[:,
'inference'] = df_test.prompt.apply(completion, args=(model, ))

# 正誤判定
df_test.inference = df_test.inference.
map(lambda x: x.replace(' ',''))
df_test.loc[:,
'judge'] = df_test.completion == df_test.inference

print(collections.Counter(df_test.judge.values)) # Counter({True: 91, False: 9})

結果

結果は次の通りです。

ファインチューニングによって、davinci で最大 15 ポイント、curieで最大 33 ポイント、babbageで最大 31 ポイントの精度向上を確認しました。ファインチューニングの効果ははっきりと表れているようです。

また、エンジン容量による精度の違いは、今回のタスクにおいては、あまり見られませんでした。僅かですが、curie で多くの学習データを使用したケースが最も高い精度となりました。

おわりに

今回のファインチューニングタスクは、二値分類という比較的単純なタスクであったためか、効果ははっきりと確認できましたが、より複雑な Completion ではどのような結果になるか、今後チャンスがあったら実験してみたいと思います。

以上、GPT-3 のファインチューニングについて、使い方や学習曲線の確認方法、そしてファインチューニングしない場合とした場合とでの精度の違いを、IMDb (Internet Movie Database) 映画レビューの感情分類を例に解説いたしました。

次回は、 Codex による JavaScript と Python コードの生成実験を、デモ動画にてご紹介する予定です。

最後までお読みいただき、ありがとうございました。

佐々木 宏

AIエンジニアブログ 関連記事

 

******

マクニカでは、AIを活用した様々なソリューションの導入事例・ユースケースをご用意しています。以下リンクより、ぜひお気軽に資料DL・お問い合わせください。

▼世界2.5万人のデータサイエンスリソースを活用したビジネス課題解決型のAIサービス 詳細はこちら