BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

LLMを利用したテキストアノテーションのツール化

本記事は BASE アドベントカレンダー 2023 の15日目の記事です。

はじめに

こんにちは。BASEのデータ分析チーム(Data Strategy Team)で不正対策を行ったり、機械学習を触ったりしている竹内です。

ChatGPT(GPT-3.5 Turbo)が2022年の11月に公開されてから、だいたい1年以上が経ったことになります。

そしてこの1年近くでChatGPTに匹敵する多数のLLMの公開や国産LLM作成の動き、拡散モデルを主軸とした画像生成AIの台頭など様々なムーブメントがあり、それを経て「生成AI」という単語は2023年の流行語大賞に選ばれるほど人口に膾炙する結果となりました。

生成AI、特にChatGPTをはじめとする対話用にチューニングされた大規模言語モデル(以下チャットLLMと表記します。)の実応用という面に関していうと、人の代わりに文章を作成させたり、知りたい情報を提示させたり、アイデアを提案させたりといった人間のアシスタントのように振る舞える性質を活かした使われ方が多い印象で、BASEにおいてもチャットLLMのそういった側面を活用したサービスの開発、リリースが進んでいます。

一方で少し見方を変えると、チャットLLMは任意のテキストを入力として受け付け、指定した手順に従ってタスクを実行し、出力のフォーマットもある程度コントロールできる、世界知識をもった自然言語処理ツールであるという側面も存在します。(精度はタスクの難易度やモデルの性能に依存します。)

ChatGPTによるクラス分類

チャットLLMのこうした側面に着目した実応用例やツール化に関しては、もしかするともうすでにいろいろな場所で検証され、実際に運用されているのかもしれませんが、様々な事情から前者の「アシスタントAI」的な例と比較してあまり表立って取り上げられていないような印象があります。

そういった背景もあり、直近で商品テキストのカテゴリ分類という典型的なタスクを現実的なコストで実行するために、チャットLLMを利用したちょっとしたツールを作る機会があったため、それについてまとめていきたいと思います。

チャットLLMによるテキスト分類

BASEでは検索、レコメンドシステムの改善や不正決済対策、不適切なコンテンツへの対策などに活用するため一部機械学習ベースのテキスト分類モデルの作成を行っています。具体的には以前も紹介した商品カテゴリの分類モデルなどが挙げられます。

このようなテキスト分類モデルを作成するには、従来であれば人の手によってラベル付け(アノテーション)された十分なサイズの学習/テストデータセットを用意する必要がありました。しかしながら十分なサイズとラベルの品質が担保されたデータセットを作成するのは容易ではなく、クラウドソーシングなどを利用した場合でも相当な時間的、金銭的コストがかかることになります。商品カテゴリの分類という、内容自体は比較的取り組みやすいタスクについてもデータセットの作成がネックとなっていました。

一方でChatGPTのようなチャットLLMは、適切な指示文(プロンプト)を与えるだけで様々なタスクを解くことができるという非常に高い汎用性、柔軟性をもっているため、テキスト分類においてもファインチューニング無しで十分な力を発揮することができ、ドメインによっては人間と比較しても遜色ない性能を発揮することも可能です。[1]

しかしながらチャットLLMをそのままテキスト分類モデルとして利用し続ける場合、ファインチューニングしたモデルで推論を行う手法と比較して

  • 推論速度
  • コスト
  • APIの利用制限(ChatGPTやBARDのようなAPIを利用する場合)
  • 出力のフォーマットの安定性

などの面で課題が発生します。

特に推論速度の面で言えば簡単なテキスト分類においても1件あたり数秒〜十数秒は要するケースが多く、モデルの最適化もバッチ処理も基本的には不可能であるために、データの量によってはそもそも推論が追いつかないことがあります。

こうしたスケーラビリティ上の課題を解決する方法としては、チャットLLMを用いてテキスト分類を行った結果を教師信号とし、それを用いてBERTなどのより軽量な事前学習モデルをファインチューニングするという方法が考えられます。

要するに人間の代わりにチャットLLMにアノテーションさせるということになりますが、複雑で巨大なモデルの有する知識を、よりデプロイメントに適したモデルへと移転させる技術である知識蒸留(knowledge distillation)[2]を行っているという見方もできるかもしれません。

(ただしChatGPTの場合、出力をOpenAIとの競合となるようなモデルの学習目的で利用することが規約で禁止されている[3]ため注意が必要になります。)

分類タスクを実行するためのプロンプトのテンプレート化

データセットへのアノテーションを目的としてテキスト分類を実行するという工程については、ある程度ツール化した方が便利であると考えました。

テキスト分類を実行させるための指示や結果のフォーマットの指定などに必要なプロンプトをいちいち書かずに、scikit-learnやPyTorchなどと同じような振る舞いをするPythonのライブラリとして使用できればドメインごとの取り回しが良いです。

例えばニュースのトピック分類やツイートのネガポジ分類など異なるドメインのテキスト分類タスクを解くためにチャットLLMを利用する際、いちいち「クラス分類してください」的なプロンプトや「この例のようにjson形式で出力してください」的なフォーマット部分や、APIを叩く部分を書くのはなかなかに煩わしいものとなります。タスクを解く上で必要最低限となる情報や要件のみを直接引数などで設定できることが望ましいです。

クラス分類タスクを実行させるためのプロンプトに関して言えば、ドメインに関わらず共通している部分と、ドメインごとに指定する必要のある部分に分けることができ、前者をある程度テンプレート化した上で後者をそのテンプレートに当てはめることが可能です。

ドメインに関わらず共通化できる部分としては

  • テキスト分類を実行させるための指示部分
  • 理由や確度、マルチラベルとするかなどの有無の指定
  • 各種設定に応じた出力のフォーマットとその例示部分

などが挙げられ、

テキスト分類においてドメインごとに指定する必要のある部分としては

  • どのような文脈のテキストを分類するか。(例: Webサービスの利用者のコメント)
  • どのようなラベルに分類するか。(例: 利用時の印象)
  • それぞれのラベルはどんなものか。(例: 「ネガティブorポジティブ」)

などが挙げられます。

以上を踏まえて、結果的に以下のように動作するチャットLLMによるテキスト分類ツールを作成しました。

from llmtask.tasks import ClassificationTask

# クラス分類されるテキストの説明
input_description = "EC review comments"

# クラスの説明
label_description = "Impressions from the comments"

# ラベル名: ラベルの説明や具体例
labels = {
    "Positive": "Receive a good impression(endorsement, praise, support, recommendation)",
    "Negative": "Receive a bad impression(disagree, criticize, attack, slander)",
    "Neutral": "Neither positive nor negative"
    }

task = ClassificationTask(
    input_description=input_description,
    label_description=label_description,
    labels=labels,
    require_reason=True,
    llm="openai",
    language="en",
    )
output = task(
    input_text="The order was delivered right away!",
    model="gpt-3.5-turbo-1106",
)

labels = output.label
label_index=output.label_index
print(label_index, labels)
# >> 0 Positive

reason = output.reason
print(reason)
# >> The comment 'The order was delivered right away!' expresses satisfaction and a positive impression.

内部ではテンプレートに従ってだいたい以下のようなイメージのプロンプトが作成され、パースされた出力結果が戻り値として返るようになっています。

"""
Perform text labeling according to the following instructions.
Let text X be EC review comments.
Let label Y be Impressions from the comments.
Select one of the labels Y that corresponds to text X.
Briefly describe the reason for the labeling.
The output format is json format as follows: {"reason": "Reason for labeling (string type)", "label": "Label to be assigned (string type) "} for example {"reason": "Because it is xx.", "label": "label"}.
label Y=['Positive', 'Negative', 'Neutral']
Details of each label: {'Positive': 'Receive a good impression(endorsement, praise, support, recommendation)', 'Negative': 'Receive a bad impression(disagree, criticize, attack, slander)', 'Neutral': 'Neither positive nor negative'}
text X='The order was delivered right away!'
"""

別の言語の分類においても同様な形式でタスクを実行することができ、exampleとしてのデータの追加やマルチラベル出力の指定なども可能となっています。

input_description = "ECサイトのレビューコメント"
label_description = "コメントから読み取れる印象"

labels = {
    "ポジティブ": "良い印象(賞賛、支持、推奨など)",
    "ネガティブ": "悪い印象(批判、攻撃、中傷など)",
    "ニュートラル": "どちらでもない中立的な印象",
    }

task = ClassificationTask(
    input_description=input_description,
    label_description=label_description,
    labels=labels,
    multi_label=False,
    require_reason=True,
    require_confidence=True,
    llm="openai",
    language="ja",
    )

task.set_examples(
    example_inputs=["とても良い品質でした。", "包装が破れていました。", "六本木に本店があります。"],
    example_labels=["ポジティブ", "ネガティブ", "ニュートラル"])

output = task(
    input_text="注文された商品がすぐに届きました!",
    model="gpt-3.5-turbo-1106",
)

labels = output.label
label_index=output.label_index
print(label_index, labels)
# >> 0 ポジティブ

reason = output.reason
print(reason)
# >> 「すぐに届きました!」という内容から、賞賛の意味でポジティブな印象に該当します。

プロンプトのテンプレートは以下のような形式で言語ごとに実装しています。(あまり洗練されていません。)

class _ClassificationPrompt:
    role: str
    task_description: str
    input_definition_holder: str
    ...

    def __init__(self, input_description: str, label_description: str, labels: dict[str, str], multi_label: bool = False, require_reason: bool = False, require_confidence: bool = False) -> None:
    self.instruction = self._build_instruction()
    ...

    def _build_instruction(self) -> str:
        instruction = ""
        instruction += self.task_description
        instruction += self.input_definition_holder.format(input_description=self.input_description)
        instruction += self.output_definition_holder.format(label_description=self.label_description)
    ...
    return instruction

class _EnglishClassificationPrompt(_ClassificationPrompt):
    role = "Machine learning model for labeling text"
    task_description = f"Perform text labeling according to the following instructions."
    input_definition_holder = "Let text X be {input_description}."
    ...

class _JapaneseClassificationPrompt(_ClassificationPrompt):
    role = "テキストのラベル付けを行う機械学習モデル"
    task_description = "次の指示に従いテキストのラベル付けを実行せよ。"
    input_definition_holder = "テキストXを{input_description}とする。"
    ...

実際に上記のツールを使用してテキストデータのラベル付けを行い、非常に低コストで十分な量および質(人手でのアノテーション結果と比較して遜色ない程度)のデータセットを作成することが可能となりました。

例えば商品カテゴリの分類に関しては最終的に百を超えるラベル数を設定する形になりましたが、ファインチューニングを行ったモデルのメトリクスに関しても十分な数値を出すことができています。

課題や展望など

出力のフォーマット

柔軟性の高さと引き換えに、出力が100%期待した形式になるとは限らないという点はチャットLLMのよく知られた欠点の1つであり、今回のようなテキスト分類においても例外ではありません。

上記のツールではラベル名を直接回答する形でプロンプトを作成していますが、与えられた選択肢に適切なものがなかった場合などは、選択肢にないラベル名を回答することも多々ありました。

ラベル名を回答させるのではなく、{1: ”ラベル1”, 2: “ラベル2”, 3: “ラベル3”}といった形式の選択肢に対して該当するラベルの番号を返すようにプロンプトを実装することも可能で、当初はこの形式を採用していました。この場合出力のフォーマットは安定するものの、精度が下がる(reasonを見る限り正しく理解はできているが、インデックスを選ぶところで間違えている)ケースが見られたため、ラベル名を直接答える方式を採用しました。

一方で選択肢にないものが多数回答されている場合には、ラベルセットを見直す参考にもなるためその点では役に立つ側面もありました。

プロンプトの長さ

現状の仕組みでは1データの推論を行う度にそのデータのテキストだけでなく、共通の指示やラベルの説明といったタスクの説明を行うシステムプロンプトも逐一投げる必要があります。

そのため特にラベル数が百を超えるような日本語のタスクでは全体のトークンサイズが非常に大きくなってしまうため、ラベルの説明部分に細かい具体例を記述せず、簡素なものにせざるを得ないということもありました。

一度ベースとなる指示を与えた後で複数データのテキストをまとめて入力することも可能ではありますが、トークン数が長くなることによって精度が下がる可能性やそもそもトークン数のサイズ制限に引っかかる可能性があるという点を留意すると、一筋縄ではいかないかもしれません。

あるいはChatGPTのファインチューニング機能をうまく活用できればこの点を解決できるかもしれず、その辺りは要検証といったところです。

閾値の設定

テキスト分類モデルと同じような振る舞いをしていますが、出力が確率ではなくラベルそのものであるため、閾値の調節ができない点は欠点として挙げられます。

一応プロンプトの中に確度(confidence)として0〜100の数値を出力させるようなオプションにも一応対応させてはみましたが、あまり信頼できない印象がありました。(全体で一貫した基準が保たれていない印象でした。)

おわりに

冒頭でも触れた通りチャットLLMに関しては、情報検索の代替的な使い方や、要約、翻訳、相談役、アイデア創出、語学学習といった「話し相手」「アシスタント」的なものが代表的なユースケースとして取り上げられることが多いですが、見方を変えれば、定義した問題を解釈し、自動的に解きながら指定したフォーマットで出力してくれるツール的な使い方も可能です。

人間の話し相手になってくれたり、人間が解けない難しい問題を解いてくれたりするAIという側面の他に、従来人間が行なっていた「単純で答えもある程度決まっているが世界知識がないと解けない」自然言語の絡むタスクを自動的に処理してくれるAIという側面に着目すると、業務の大幅な効率化やコスト削減が図れるかもしれません。

また、実際プロンプト次第ではテキスト分類のアノテーションだけでなくテキストのクラスタリング[4]やデータの拡張[5]などについても応用例が考えられており、従来の機械学習技術とうまく組み合わせることでも新しい価値や、インパクトのある使い道が発見できるかもしれません。

最近ではチャットLLMは画像とテキストの入出力対応によるマルチモーダル化という次のステップに進みつつあり、LLMを利用する人間側も頭を柔らかくして、いろいろな利用可能性を模索、検証していくことが求められます。

最後となりますが、弊社では機械学習エンジニアを募集しております! ご興味のある方はお気軽にご応募ください! https://herp.careers/v1/base/nXWWtX2Kjm1I

明日は@endu さんの記事です。お楽しみに!

References

[1] Fabrizio Gilardi, Meysam Alizadeh, Maël Kubli, ”ChatGPT Outperforms Crowd-Workers for Text-Annotation Tasks”, Mar 2023.

[2] G. Hinton, O. Vinyals, and J. Dean, “Distilling the knowledge in a neural network”, Mar. 2015.

[3] https://openai.com/policies/terms-of-use

[4] Yuwei Zhang, Zihan Wang, Jingbo Shang, ”ClusterLLM: Large Language Models as a Guide for Text Clustering”, May 2023

[5] Zhihong Shao, Yeyun Gong, Yelong Shen, Minlie Huang, Nan Duan, Weizhu Chen, ”Synthetic Prompting: Generating Chain-of-Thought Demonstrations for Large Language Models”, Feb 2023