🤖

大規模言語モデル(LLM)で独自データセットを評価する

2023/10/06に公開

本記事では、lm-evaluation-harness という大規模言語モデル評価ツールを利用して、テキスト分類データセット awesome-japanese-nlp-classification-dataset に対する評価方法とその結果を共有しようと思います。

具体的には、BERT によるファインチューニングモデル、Open-Orca 系モデル(7B,13B)による Few-shot、OpenAI API の比較結果とその簡単な分析結果を共有します。

Model F1-score
Random 0.13
gpt2 0.13
eleutherAI/gpt-j-6B 0.13
pankajmathur/orca_mini_v3_7b 0.52
Open-Orca/OpenOrca-Platypus2-13B 0.65
PulsarAI/Luban-Marcoroni-13B-v1 0.74
bert-base-multilingual-cased (Finetuning) 0.74
gpt-3.5-turbo 0.46
gpt4 0.67

本記事をご覧になることで、独自に作成したデータセットを大規模言語モデルで評価する方法を把握することができます。

本記事の対象読者

以下の方々を対象に、本記事を執筆しました。

  • lm-evaluation-harness の使い方を知りたい方
  • 大規模言語モデルを評価する方法を知りたい方
  • BERT ファインチューニングモデルによる分類、大規模言語モデルによる Few-shot 分類、OpenAI API による分類、どの方法の精度が良いか知りたい方

データセット概要と検証の目的

まずは、評価対象とするデータセットの概要を説明します。
https://huggingface.co/datasets/taishi-i/awesome-japanese-nlp-classification-dataset

このデータセットは、GitHub リポジトリの概要(入力テキスト)が日本語の自然言語処理(NLP)に関連しているかどうかの情報として「関連あり(1)」と「関連なし(0)」のラベルが付与されています。

このデータセットを利用することで、大規模言語モデルは GitHub リポジトリを仕分けすることができるか、その能力を評価したいと想定しています。

この評価を行うことで、これまで人間が取り組んできた情報抽出作業をどれほど大規模言語モデルは再現できるか、今回はその検証を行いたいと思います。

lm-evaluation-harness について

次に、大規模言語モデルを評価するツールである lm-evaluation-harness の概要を説明します。

簡単に説明すると、lm-evaluation-harness は、次のスクリプトを実行することで、あるタスク(ここでは、hellaswag)に対して、あるモデル(ここでは、pretrained=EleutherAI/gpt-j-6B)を利用した場合、どれほどの精度が出るか評価するツールです。

lm-evaluation-harness の実行例
python main.py \
    --model hf-causal \
    --model_args gpt2 \
    --tasks hellaswag \
    --device cuda:0

上記のスクリプトを実行すると、次のような結果を簡単に確認することができ、引数を変更するだけで、様々なモデルの検証を行うことができるため、多くの研究者やエンジニアに利用されています。

hf-causal (pretrained=gpt2), limit: None, provide_description: False, num_fewshot: 0, batch_size: None
|  Task   |Version| Metric |Value |   |Stderr|
|---------|------:|--------|-----:|---|-----:|
|hellaswag|      0|acc     |0.2888|±  |0.0045|
|         |       |acc_norm|0.3109|±  |0.0046|

しかし、既に lm-evaluation-harness に組み込まれているデータセットを評価することは簡単ですが、独自に準備したデータセットを評価するためには、少し工夫が必要です。

そこで、私が独自データセットを評価するために取り組み得た知見を共有することで、同様の作業の行う方々の負担を少しでも軽減できればと考えています。

lm-evaluation-harness の使い方

ここからは、実際に独自データセットを評価するための作業を説明します。

今回は独自データセットの評価を行いますので、Task Guide のページを参考にします。

このページでは、二値分類のテキスト分類を新たなタスクとして追加し、その評価を行う方法を説明しています。

まずセットアップとして、lm-evaluation-harness リポジトリをクローンし、新たなブランチを作成し、必要なライブラリをインストールします。

ここでは awesome_ja_nlp という新たなブランチを作成します。
必要に応じて、ブランチ名(awesome_ja_nlp)を変更してください。

git clone https://github.com/EleutherAI/lm-evaluation-harness
cd lm-evaluation-harness
git checkout e81d3cce155e93ba2445068767c738891ad97024
git checkout -b awesome_ja_nlp
pip install -e ".[dev]"

ライブラリのインストールが完了したら、次にタスクテンプレートファイルをコピーし、新しいタスクのファイルを作成します。

cp templates/new_task.py lm_eval/tasks/awesome_ja_nlp.py

こちらが実際のコードです。
重要なポイントは、個別その処理を説明しています。

awesome_ja_nlp.py
from lm_eval.base import Task
from lm_eval.base import rf
from lm_eval.metrics import mean, f1_score

_CITATION = """
"""

class AwesomeJANLP(Task):
    VERSION = 0
    DATASET_PATH = "taishi-i/awesome-japanese-nlp-classification-dataset"
    DATASET_NAME = None

    def has_training_docs(self):
        return False

    def has_validation_docs(self):
        return False

    def has_test_docs(self):
        return True

    def training_docs(self):
        if self.has_training_docs():
            if self._training_docs is None:
                self._training_docs = list(self.dataset["train"])
            return self._training_docs

    def validation_docs(self):
        if self.has_validation_docs():
            return self.dataset["validation"]

    def test_docs(self):
        if self.has_test_docs():
            return self.dataset["test"]


    def doc_to_text(self, doc):
        context = f"""### Question:\nIs this description relevant to Japanese natural language processing? Please answer Yes or No.\n\n### Description:\nThe Symfony PHP framework\n\n### Answer: No\n\n### Question:\nIs this description relevant to Japanese natural language processing? Please answer Yes or No.\n\n### Description:\nGLUE: Japanese General Language Understanding Evaluation\n\n### Answer: Yes\n\n### Question:\nIs this description relevant to Japanese natural language processing? Please answer Yes or No.\n\n### Description:\n{doc['text']}\n\n### Answer: """
        return context

    def doc_to_target(self, doc):
        return "{}".format({0: "No", 1: "Yes"}[doc["label"]])

    def construct_requests(self, doc, ctx):
        ll_relevant, _ = rf.loglikelihood(ctx, "Yes")
        ll_not_relevant, _ = rf.loglikelihood(ctx, "No")
        return ll_relevant, ll_not_relevant

    def process_results(self, doc, results):
        ll_relevant, ll_not_relevant = results
        pred = 1 if ll_relevant > ll_not_relevant else 0
        gold = int(doc["label"])
        return {"acc": int(pred) == int(gold), "f1": (int(gold), int(pred))}

    def aggregation(self):
        return {"acc": mean, "f1": f1_score}

    def higher_is_better(self):
        return {"acc": False, "f1": True}

ライブラリのインポート

まずは、必要なライブラリをインストールします。
ここでは、loglikelihood を計算する rf 関数と、評価スコアを求める mean, f1_score をインポートします。

from lm_eval.base import Task
from lm_eval.base import rf
from lm_eval.metrics import mean, f1_score

データセットの指定

次に、タスク名を指定します。
タスク名として、AwesomeJANLP をクラス名にします。
このクラス名は、後ほど別のファイルでインポートする必要があるので、覚えておいてください。

DATASET_PATH は、Hugging Face の Dataset のプロジェクト名を指定してください。
ここでは、"taishi-i/awesome-japanese-nlp-classification-dataset"を指定しています。

データセットの Subset を選択する必要がある場合は、DATASET_NAMEに指定してください。
選択する必要がなければ、Noneとしてください。

class AwesomeJANLP(Task):
    VERSION = 0
    DATASET_PATH = "taishi-i/awesome-japanese-nlp-classification-dataset"
    DATASET_NAME = None

利用するデータの種類の指定

今回は、プロンプトに Few-shot の事例をハードコーディングしているためhas_training_docsFalse にしています。

もし Few-shot に学習データを利用する場合は、has_training_docsTrue にしてください。

ここで、重要なポイントとして、評価データを利用するため、has_test_docsTrue にします。

def has_training_docs(self):
    return False

def has_validation_docs(self):
    return False

def has_test_docs(self):
    return True

モデルの入力となるプロンプトの設定

関数 doc_to_text では、モデルの入力となるプロンプトを記載します。
今回は、次のように Few-shot の事例をハードコーディングしています。

def doc_to_text(self, doc):
    context = f"""### Question:\nIs this description relevant to Japanese natural language processing? Please answer Yes or No.\n\n### Description:\nThe Symfony PHP framework\n\n### Answer: No\n\n### Question:\nIs this description relevant to Japanese natural language processing? Please answer Yes or No.\n\n### Description:\nGLUE: Japanese General Language Understanding Evaluation\n\n### Answer: Yes\n\n### Question:\nIs this description relevant to Japanese natural language processing? Please answer Yes or No.\n\n### Description:\n{doc['text']}\n\n### Answer: """
    return context

GitHub リポジトリの概要を入力として、大規模言語モデルに「日本語NLPに関連するか否か」という質問を与えて、「YES」または「NO」を出力させるプロンプトです。

### Question:
Is this description relevant to Japanese natural language processing? Please answer Yes or No.

### Description:
The Symfony PHP framework

### Answer: No

### Question:
Is this description relevant to Japanese natural language processing? Please answer Yes or No.

### Description:
GLUE: Japanese General Language Understanding Evaluation

### Answer: Yes

### Question:
Is this description relevant to Japanese natural language processing? Please answer Yes or No.

### Description:
Dialogue Commonsense Graph in Japanese

### Answer: 

ラベルの変換

ここでは、データセットのラベル(01)をプロンプトの出力に合わせた正解ラベル(YesNo)に変換しています。

def doc_to_target(self, doc):
    return "{}".format({0: "No", 1: "Yes"}[doc["label"]])

言語モデルのスコア(対数尤度)の計算

関数 rf.loglikelihood を利用して、上記のプロンプトに対して、「Yes」の場合の対数尤度、「No」の場合の対数尤度を計算します。その値を戻り値として返します。

def construct_requests(self, doc, ctx):
    ll_relevant, _ = rf.loglikelihood(ctx, "Yes")
    ll_not_relevant, _ = rf.loglikelihood(ctx, "No")
    return ll_relevant, ll_not_relevant

ラベルの出力、正解ラベルとの比較

process_results では、関数 rf.loglikelihood で計算したスコア(対数尤度)を比較し、スコアの高いプロンプトのラベル(01)を出力します。

そして、予測ラベルと正解ラベルと比較し、出力が正解かどうか、その値を戻り値として返します。

def process_results(self, doc, results):
    ll_relevant, ll_not_relevant = results
    pred = 1 if ll_relevant > ll_not_relevant else 0
    gold = int(doc["label"])
    return {"acc": int(pred) == int(gold), "f1": (int(gold), int(pred))}

評価指標の設定

ここでは、評価結果として出力する評価指標を設定します。
上記の process_results と出力を統一化してください。

def aggregation(self):
    return {"acc": mean, "f1": f1_score}

def higher_is_better(self):
    return {"acc": False, "f1": True}

新しいタスクの登録

上記で作成した独自データセットのタスク lm_eval/tasks/awesome_ja_nlp.pylm-evaluation-harness に登録します。

具体的には、lm_eval/tasks/__init__.py にコードを追加します。

まず、lm_eval/tasks/__init__.py の70行目に、 from . import awesome_ja_nlp を追加します。

from . import cmmlu
from . import awesome_ja_nlp

########################################
# Translation tasks
########################################

次に、TASK_REGISTRY"awesome_ja_nlp": awesome_ja_nlp.AwesomeJANLP, を追加します。lm_eval/tasks/__init__.py の341行目あたりに追加してください。

TASK_REGISTRY = {
    ...
    "awesome_ja_nlp": awesome_ja_nlp.AwesomeJANLP,
    ...
}

以上、2箇所にコードを追加することで、新しいタスクを登録することができます。
タスク名(ここでは、awesome_ja_nlp)は、各自変更してください。

評価の実行

ここから各モデルの評価を行います。

このタスクでは「日本語リポジトリを正しく見つけ出すことができるか」という目的なので、ラベル 1 の F1-score が重要な指標となります。

そのため、ラベル1 の F1-score を基準にモデルの比較を行います。

各モデルの結果を説明する前に、まずは全ての予測ラベルが 1, 0 の場合、ランダムにラベルを予測した場合の指標を説明します。

ラベルを全て 1 と予測した場合

全てのラベルが 1 と予測した場合の結果です。
ラベル1 の F1-score は、0.13 となります。

              precision    recall  f1-score   support

           0       0.00      0.00      0.00       796
           1       0.07      1.00      0.13        60

    accuracy                           0.07       856
   macro avg       0.04      0.50      0.07       856
weighted avg       0.00      0.07      0.01       856

ラベルを全て 0 と予測した場合

次に、全てのラベルが 0 と予測した場合の結果です。
ラベル1 の F1-score は、0 となります。

              precision    recall  f1-score   support

           0       0.93      1.00      0.96       796
           1       0.00      0.00      0.00        60

    accuracy                           0.93       856
   macro avg       0.46      0.50      0.48       856
weighted avg       0.86      0.93      0.90       856

ランダムにラベルを予測した場合

最後に、ランダムにラベルを予測した場合の結果です。
ラベル1 の F1-score は、0.13 となります。

              precision    recall  f1-score   support

           0       0.94      0.51      0.66       796
           1       0.08      0.53      0.13        60

    accuracy                           0.51       856
   macro avg       0.51      0.52      0.40       856
weighted avg       0.88      0.51      0.62       856

ベースラインモデルの結果

ここでは、bert-base-multilingual-cased を用いてファインチューニングしたモデルの結果について説明します。

約5500件の学習データを用いて、テキスト分類モデルを学習しました。パラメータチューニングには、Optuna を利用し、ベースラインモデルを構築しました。

そのベースラインモデルの結果がこちらです。

              precision    recall  f1-score   support

           0       0.98      0.99      0.98       796
           1       0.79      0.70      0.74        60

    accuracy                           0.97       856
   macro avg       0.89      0.84      0.86       856
weighted avg       0.96      0.97      0.97       856

このモデルは、Hugging Face で公開していますので、下記のページでテキストを入力し日本語NLPに関連するか検証することも可能です。
https://huggingface.co/taishi-i/awesome-japanese-nlp-classification-model

GPT2

ここから、lm-evaluation-harness を利用した評価方法と各モデルの評価結果を確認します。

まずは、GPT2 を用いた結果を確認します。

次のコマンドで、評価を実行することができます。

--tasks awesome_ja_nlp と指定することで、今回追加したデータセットの評価を行います。

python main.py \
    --model gpt2 \
    --model_args device=cuda:0 \
    --tasks awesome_ja_nlp \
    --num_fewshot 0

ラベル1 の F1-score が 0.1314 となり、今回のプロンプトを用いた GPT2 では、ランダムの結果と変わらないため、適切に分類できていない結果となりました。おそらくプロンプトが適切に設計されておらず、GPT2 の性能を引き出せていない可能性があります。

|     Task     |Version|Metric|Value |   |Stderr|
|--------------|------:|------|-----:|---|-----:|
|awesome_ja_nlp|      0|acc   |0.0736|±  |0.0089|
|              |       |f1    |0.1314|±  |0.0153|

eleutherAI/gpt-j-6B

次に、eleutherAI/gpt-j-6B を用いた結果を確認します。

python main.py \
    --model hf-causal \
    --model_args pretrained=EleutherAI/gpt-j-6B \
    --tasks awesome_ja_nlp \
    --device cuda:0

こちらも ラベル1 の F1-score が 0.1333 となり、ランダムの結果と変わらないため、適切に分類できていない結果となりました。しかし、このモデルは GPT-J-6B is not intended for deployment without fine-tuning, supervision, and/or moderation.と説明があり、その内容通りにファインチューニングなどの調整なしには、テキスト分類にはそのまま利用できないことがわかります。

|     Task     |Version|Metric|Value |   |Stderr|
|--------------|------:|------|-----:|---|-----:|
|awesome_ja_nlp|      0|acc   |0.1040|±  |0.0104|
|              |       |f1    |0.1333|±  |0.0157|

pankajmathur/orca_mini_v3_7b

次に、open_llm_leaderboard を参考に、2023年10月6日時点で、上位スコアのモデル(7B)を検証します。

pankajmathur/orca_mini_v3_7b の結果を確認します。

python main.py \
    --model hf-causal \
    --model_args pretrained=pankajmathur/orca_mini_v3_7b \
    --tasks awesome_ja_nlp \
    --device cuda:0

また、今回の検証では、モデルロード時に load_in_8bit=True を有効にする必要があったため、lm_eval/models/gpt2.py の次の箇所(79行目から86行目)をハードコーディングし変更しています。

self.model = transformers.AutoModelForCausalLM.from_pretrained(
    pretrained,
    # load_in_8bit=load_in_8bit,
    load_in_8bit=True,
    # low_cpu_mem_usage=low_cpu_mem_usage,
    revision=revision,
    # torch_dtype=_get_dtype(dtype),
    torch_dtype=torch.float16,
    # trust_remote_code=trust_remote_code,
)

上記を変更した場合は、追加で下記のライブラリをインストールしてください。

pip install accelerate bitsandbytes

今回は、ラベル1 の F1-score が 0.5153 となり、適切に分類できている結果を得ることができました。2件だけの Few-shot 分類ですが、そこそこ高い精度です。

|     Task     |Version|Metric|Value |   |Stderr|
|--------------|------:|------|-----:|---|-----:|
|awesome_ja_nlp|      0|acc   |0.9077|±  |0.0099|
|              |       |f1    |0.5153|±  |0.0481|

Open-Orca/OpenOrca-Platypus2-13B

さらに、open_llm_leaderboard を参考に、2023年10月6日時点で、上位スコアのモデル(13B)を検証します。

まずは、Open-Orca/OpenOrca-Platypus2-13B の結果を確認します。

python main.py \
    --model hf-causal \
    --model_args pretrained=Open-Orca/OpenOrca-Platypus2-13B \
    --tasks awesome_ja_nlp \
    --device cuda:0

今回は、ラベル1 の F1-score が 0.6528 となり、適切に分類できている結果を得ることができました。2件だけの Few-shot 分類ですが、かなり高い精度です。

|     Task     |Version|Metric|Value |   |Stderr|
|--------------|------:|------|-----:|---|-----:|
|awesome_ja_nlp|      0|acc   |0.9416|±  |0.0080|
|              |       |f1    |0.6528|±  |0.0466|

PulsarAI/Luban-Marcoroni-13B-v1

次に、Open-Orca/OpenOrca-Platypus2-13B の結果を確認します。

python main.py \
    --model hf-causal \
    --model_args pretrained=PulsarAI/Luban-Marcoroni-13B-v1 \
    --tasks awesome_ja_nlp \
    --device cuda:0

ここで、ラベル1 の F1-score が 0.7391 となり、bert-base-multilingual-cased をファインチューニングした結果と同等の精度を確認することができました。

こちらも2件だけの Few-shot 分類ですが、かなり高い精度です。ファインチューニング用の学習データ5490件と同じだけの分類能力を出すことができることがわかりました。

|     Task     |Version|Metric|Value |   |Stderr|
|--------------|------:|------|-----:|---|-----:|
|awesome_ja_nlp|      0|acc   |0.9579|±  |0.0069|
|              |       |f1    |0.7391|±  |0.0426|

OpenAI API の結果

最後に、OpenAI API を利用した場合の結果を確認します。

ここでは、lm-evaluation-harness で利用したプロンプトをそのまま利用し、OpenAI の 公式の Python ライブラリによる検証を行いました。

サンプルコードのため、そのままでは動作しません。
import openai
...
model_name = "gpt-3.5-turbo"
...
context = f"""### Question:\nIs this description relevant to Japanese natural language processing? Please answer Yes or No.\n\n### Description:\n(End of life: May 31, 2023) AWS Toolkit for Eclipse\n\n### Answer: No\n\n### Question:\nIs this description relevant to Japanese natural language processing? Please answer Yes or No.\n\n### Description:\nJapanese analyzer uses kuromoji japanese tokenizer for ElasticSearch\n\n### Answer: Yes\n\n### Question:\nIs this description relevant to Japanese natural language processing? Please answer Yes or No.\n\n### Description:\n{doc['text']}\n\n### Answer:\n"""           
messages = [{"role": "system", "content": context}]
completion = openai.ChatCompletion.create(
    model=model_name,
    messages=messages,
)

gpt-3.5-turbo

model_name = "gpt-3.5-turbo" を設定した場合の結果です。

ラベル1 の F1-score が 0.46 となり、ある程度テキスト分類できている結果を得ることができました。OpenAI API用に、プロンプトを調整したわけではないですが、そこそこ良い結果かと思われます。

              precision    recall  f1-score   support

          No       0.99      0.86      0.92       796
         Yes       0.31      0.83      0.46        60

    accuracy                           0.86       856
   macro avg       0.65      0.85      0.69       856
weighted avg       0.94      0.86      0.89       856

gpt4

model_name = "gpt4" を設定した場合の結果です。

ラベル1 の F1-score が 0.67 となり、適切に分類できている結果を得ることができました。だた、モデルを gpt4 に変更しただけですが、gpt-3.5-turbo よりも精度が高いようです。

              precision    recall  f1-score   support

          No       0.99      0.95      0.97       796
         Yes       0.57      0.82      0.67        60

    accuracy                           0.94       856
   macro avg       0.78      0.89      0.82       856
weighted avg       0.96      0.94      0.95       856

まとめ

今回の検証で注目したいポイントとしては、Open-Orca/OpenOrca-Platypus2-13B を利用した場合、2件のみの Few-shot 分類で、BERT の ファインチューニングモデルと同等の結果を得られたことです。

Model F1-score
Random 0.13
gpt2 0.13
eleutherAI/gpt-j-6B 0.13
pankajmathur/orca_mini_v3_7b 0.52
Open-Orca/OpenOrca-Platypus2-13B 0.65
PulsarAI/Luban-Marcoroni-13B-v1 0.74
bert-base-multilingual-cased (Finetuning) 0.74
gpt-3.5-turbo 0.46
gpt4 0.67

ファインチューニング用の学習データは、約1年かけて収集したデータをベースに構築しています。

このデータは、本記事の筆者が主観的に収集し続けてきたデータですが、その行動をたった2件のサンプルのみで7割以上の模倣できると考えると、大規模言語モデルの言語理解能力は非常に強力だと改めて感じる結果となりました。


(2023/10/12 追記 bert-base-multilingual-cased Finetuning で学習データセット数の違いによる精度の変化)

本記事では、テキスト分類のタスクを想定していますが、lm-evaluation-harnesstasksのコードを参考に、他のタスクにも拡張できるかと思いますので、この記事が役に立つと幸いです。

もし上記のコードでエラーが発生する場合は、できる限りレスポンスしようと思いますので、気軽にコメントをいただければと思います。

最後になりますが、この記事を読んでいただき、ありがとうございました。

Discussion