Zero-Shotテキスト分類の基礎と実践

日々膨大なテキストデータが生み出される現代において、テキスト分類は情報整理や分析に欠かせない技術です。スパムメールのフィルタリング、ニュース記事のカテゴリ分け、顧客レビューの感情分析など、様々な応用があります。

しかし、従来のテキスト分類モデルを構築するには、通常、大量のラベル付きデータが必要です。新しい分類カテゴリを追加するたびに、多大な時間とコストをかけてデータを収集し、手作業でラベル付けを行う必要がありました。これは、特にニッチな分野や変化の速い領域では大きな負担となります。

そこで注目されているのが「Zero-Shotテキスト分類」です。この手法は、モデルが学習時に一度も見たことのないラベルに対しても、そのラベルの意味や文脈を理解して分類を行うことができる技術です。ラベル付きデータ作成の手間を大幅に削減し、未知のカテゴリにも柔軟に対応できるという強力な利点があります。

今回は、日本語のニュースデータセットであるLivedoorニュースコーパスを題材に、Zero-Shotテキスト分類を実際にコードを動かしながら解説します。

2. Zero-Shotテキスト分類の仕組み

Zero-Shotテキスト分類の最大の疑問は、「学習時に一度も見たことのないカテゴリを、なぜ分類できるのか?」という点でしょう。その鍵となるのは、「事前学習済みモデル」が持つ強固な言語理解能力と、「テキストとラベル間の意味的な関連性」を捉える能力です。

Zero-Shot分類モデルは、インターネット上の膨大なテキストデータなどで事前学習を行うことで、単語やフレーズの意味、文脈、そして様々な概念間の関係性について豊富な知識を獲得しています。この知識を基盤として、未知のカテゴリに属するテキストを分類しようとします。

具体的には、Zero-Shot分類では、分類したいテキストと、候補となるラベル(カテゴリ名)やその説明文をモデルに提示し、これらの間にどのような関係があるかを判断させます。この「関係性の判断」のアプローチによって、いくつかの主要な手法に分けられます。

NLI (Natural Language Inference) ベースの手法

このアプローチでは、テキスト分類タスクを「自然言語推論(NLI)」タスクに変換します。NLIとは、「ある前提文が与えられたとき、別の仮説文が正しい(含意)、間違っている(矛盾)、判断できない(中立)のいずれであるかを判定する」タスクです。 Zero-Shot分類に応用する場合、入力テキストを前提文、候補となるラベル名(あるいはラベルの説明文)を埋め込んだ文章を仮説文とします。例えば、「この記事のテーマは「スポーツ」です。」のような仮説文を作成し、元のテキストがこの仮説を含意するかどうかをモデルに判断させます。最も含意の度合いが高い(「含意」の確率が高い)仮説に対応するラベルを予測結果とします。BERTのようなTransformerモデルをNLIデータセットでファインチューニングすることで、このタスクに高い性能を発揮できるようになります。Hugging Face transformers ライブラリの zero-shot-classification パイプラインがこの手法を簡単に実現します。

埋め込みと類似度ベースの手法

この手法では、分類したいテキストと、候補ラベル(通常はラベル名よりも詳細な「ラベルの説明文」)を、共通のベクトル空間に埋め込みます。Sentence Transformerのようなモデルは、このような文や句の意味を捉えた高密度ベクトル(埋め込みベクトル)を生成することに特化しています。 テキストと各ラベル説明文の埋め込みベクトルが得られたら、それらの間の類似度(最も一般的なのはコサイン類似度)を計算します。最も類似度が高いラベル説明文に対応するラベルを、そのテキストのカテゴリとして予測します。ラベルの説明文を用いることで、ラベル名だけでは曖昧な場合でも、そのカテゴリがどのような内容を含むのかをモデルがより深く理解しやすくなります。

大規模言語モデル (LLM) ベースの手法

GPTシリーズやLlama、Claudeなどの大規模言語モデルは、事前学習によって極めて高い言語理解能力と推論能力を獲得しています。これらのモデルは、特別なファインチューニングなしに、テキストと候補ラベルをプロンプトとして与えるだけで、直接分類タスクを実行できます。

例えば、「以下の記事を、提示されたカテゴリの中から最も適切なものに分類してください。カテゴリ:[カテゴリA, カテゴリB, …] 記事:[テキスト本文]」のような指示を与えることで、モデルが自身の知識と入力テキストを基に最適なカテゴリを応答します。この手法では、プロンプトの表現(プロンプトエンジニアリング)や、場合によってはFew-Shot例をいくつか加える(これは厳密にはFew-Shotですが、Zero-Shotの延長として利用されることがあります)ことで、性能を調整することが可能です。

その他の手法

上記以外にも、Zero-Shot分類のアプローチは研究されています。例えば、カテゴリを「スポーツである」「屋外である」「球技である」のようなアトリビュート(属性)の組み合わせで表現し、テキストがどのアトリビュートに該当するかを予測することで最終的なカテゴリを決定する「アトリビュートベース」の手法や、カテゴリ間の階層構造や関連性をグラフとして捉え、情報を伝播させて分類を行う「グラフベース」の手法などがあります。これらの手法も、事前学習モデルの能力や、カテゴリに関する外部知識を活用するという点で共通しています。

これらの様々な手法は、いずれも事前学習モデルがテキストとラベル間の隠れた意味的な関連性を捉える能力を基盤としています。次のセクションからは、実際にコードを見ながら、これらの主要な手法がどのように実装されるのかを具体的に解説していきます。

Zero-Shotテキスト分類の実践

前章でZero-Shotテキスト分類の基本的な仕組みやアプローチの概要を理解しました。ここでは、実際にコードを動かしながら、Livedoorニュースコーパスという日本語データセットを使ってZero-Shot分類を実践し、その性能を評価していきます。

まずは使用するPythonパッケージをインストールします。

# PyTorchのインストール(使用するバージョンは適宜調整のこと)
$ pip install torch

# Hugging Face Transformers と SentenceTransformers 
$ pip install transformers sentence-transformers

# LLMのライブラリとして今回はDSPyを利用
$ pip install dspy

# その他
$ pip install fugashi hf-xet

Livedoorニュースコーパスの読み込み

今回の実践で使用するデータセットは、公開されている日本語のニュース記事データセット「Livedoorニュースコーパス」です。このデータセットは、9つのカテゴリ(生活、IT、家電、男性、映画、女性、モバイル、スポーツ、ニュース)に分類された記事テキストを含んでいます。

データセットのダウンロード、解凍、そしてテキストとラベルの抽出を行うためのPythonコードを作成します。

以下 load_livedoor_corpus 関数を実行すると、データセットが自動的にダウンロード・解凍され、各記事の本文が抽出されてテキストのリスト(texts)、対応するカテゴリラベルがラベルのリスト(labels)として返されます。記事ファイルの冒頭2行はヘッダー情報なので、3行目以降を本文として扱っています。

また、コード中には LABEL_MAPLABEL_DESCRIPTIONS という辞書を定義しています。LABEL_MAP は元のディレクトリ名と分かりやすい日本語ラベルを対応付けたもので、LABEL_DESCRIPTIONS は各ラベルがどのような内容のカテゴリであるかを説明した文章です。このラベルの説明文は、後述する手法、特に埋め込みベースの手法(または、LLMを使ったプロンプト)で重要な役割を果たします。

# livedoor_datasets.py

import os
import tarfile
import urllib.request
from glob import glob

DATASET_TGZ_FILE = "ldcc-20140209.tar.gz"
DATASET_ENDPOINT_URL = f"https://www.rondhuit.com/download/{DATASET_TGZ_FILE}"
DATASET_DIR = "livedoor_corpus"

LABEL_MAP = {
    "dokujo-tsushin": "生活",
    "it-life-hack": "IT",
    "kaden-channel": "家電",
    "livedoor-homme": "男性",
    "movie-enter": "映画",
    "peachy": "女性",
    "smax": "モバイル",
    "sports-watch": "スポーツ",
    "topic-news": "ニュース",
}

LABEL_DESCRIPTIONS = {
    "生活": "独身女性の視点から、日々の恋愛、結婚観、仕事の悩み、さらには様々なライフスタイルに関する共感できるコラムや体験談が豊富に掲載されています。",
    "IT": "最新のテクノロジー動向、便利なITツールやソフトウェアの活用法、人気Webサービスの詳細レビュー、そしてデジタルトレンドを深く掘り下げて紹介します。",
    "家電": "最新家電の新製品リリース情報や詳細な使用レビュー、役立つ便利な使い方のコツ、購入時の比較検討に役立つ選び方ガイドなど、家電に関する情報全般。",
    "男性": "男性向けの多岐にわたる興味に応える記事が集められており、最新のファッショントレンド、美味しいグルメ情報、奥深いカルチャー、充実したライフスタイル提案。",
    "映画": "公開される新作映画の最新情報や批評、注目作品の詳細なレビュー、人気俳優や監督へのインタビュー記事、映画業界の動向や舞台裏に関するニュースを提供。",
    "女性": "女性の関心が高い美容に関する秘訣や最新情報、流行のファッション解説、恋愛や人間関係のアドバイス、グルメ、そして旬のトレンド情報を幅広く掲載。",
    "モバイル": "最新スマートフォンや携帯電話のスペック、各キャリアの料金プランの比較、端末を最大限に活用するための便利な設定やアプリ情報などモバイル関連。",
    "スポーツ": "プロ野球やサッカーをはじめとする様々なスポーツの試合結果速報や詳細なレポート、注目選手の最新情報やインタビュー、移籍情報や大会関連ニュース。",
    "ニュース": "国内外で起きている様々な出来事に関する一般ニュース、政治や経済、社会問題に関する解説記事、日々の暮らしに関わるタイムリーな話題を提供。",
}


def download_livdoor_corpus() -> None:
    if not os.path.exists(DATASET_TGZ_FILE):
        urllib.request.urlretrieve(DATASET_ENDPOINT_URL, DATASET_TGZ_FILE)

    if not os.path.exists(DATASET_DIR):
        os.makedirs(DATASET_DIR)

    if not os.path.exists(os.path.join(DATASET_DIR, "text")):
        with tarfile.open(DATASET_TGZ_FILE, "r:gz") as tar:
            tar.extractall(DATASET_DIR)


def load_livedoor_corpus() -> tuple[list[str], list[str]]:
    texts = []
    labels = []

    download_livdoor_corpus()

    for label_dir_name, label_name in LABEL_MAP.items():
        dataset_files = os.path.join(DATASET_DIR, "text", label_dir_name, "*.txt")
        for filepath in glob(dataset_files):
            if "LICENSE.txt" in filepath:
                continue
            with open(filepath, encoding="utf-8") as f:
                lines = f.readlines()
                # 3行目以降が本文
                text = "".join(lines[2:]).strip()
                if text:
                    texts.append(text)
                    labels.append(label_name)

    return texts, labels

評価方法の定義

Zero-Shot分類モデルがどれだけ正しく分類できたかを定量的に評価するために、「正解率 (Accuracy)」を使用します。正解率は、モデルが予測したラベルと実際の正解ラベルが一致した割合を示す最も一般的な評価指標です。

正解率を計算するためのシンプルな関数も用意しました。以下はそのコードです。

# accuracy_eval.py


def calculate_match_frequency(predicts: list[str], labels: list[str]) -> float:
    # 2つのリストの長さが異なる場合、エラーを出す
    if len(predicts) != len(labels):
        raise ValueError("2つのリストの長さは同じでなければなりません。")

    # 一致する要素の数をカウントする
    match_count = sum(1 for a, b in zip(predicts, labels, strict=False) if a == b)

    # 一致する頻度を計算する
    frequency = match_count / len(labels)

    return frequency

この calculate_match_frequency 関数は、モデルによる予測結果のリスト predicts と、データセットから取得した正解ラベルのリスト labels を受け取り、両者の長さが同じであることを確認した上で、要素ごとに一致している数を数え、全体の数で割ることで正解率を返します。

これで、Zero-Shotテキスト分類を実践するためのデータセットと評価方法の準備が整いました。次のセクションからは、いよいよ具体的なZero-Shot分類手法の実装に入っていきましょう。

手法1:BERT × NLI による Zero-Shot 分類

Zero-Shotテキスト分類の代表的なアプローチの一つに、事前学習済みのTransformerモデル、特にBERTのようなモデルをベースとし、タスクを自然言語推論(NLI)に変換する手法があります。この手法は、Hugging Face の transformers ライブラリで簡単に実行できます。

この手法の考え方はシンプルです。分類したいテキストと、候補となる各カテゴリラベルを組み合わせ、「テキストが、ラベルに対応する仮説文をどれだけ強く推論するか?」というNLI問題としてモデルに提示します。例えば、テキストが「今日の試合は素晴らしいホームランが飛び出した」であり、候補ラベルが「スポーツ」「ニュース」だった場合、モデルは以下の2つのペアを評価します。

  1. 前提: 今日の試合は素晴らしいホームランが飛び出した 仮説: この記事のテーマは「スポーツ」です。
  2. 前提: 今日の試合は素晴らしいホームランが飛び出した 仮説: この記事のテーマは「ニュース」です。

モデルはそれぞれのペアに対して、「含意(Entailment)」「矛盾(Contradiction)」「中立(Neutral)」の確率を出力します。Zero-Shot分類では、通常、「含意」の確率が最も高い仮説に対応するラベルを予測結果とします。

Hugging Face の transformerspipeline 機能を使うと、このようなNLIベースのZero-Shot分類パイプラインを容易に構築できます。今回は、日本語のNLIデータセットでファインチューニングされたBERTモデルである Formzu/bert-base-japanese-jsnli を利用します。

以下に、この手法の実装コードを示します。

# bert_nli.py

import torch
from tqdm import tqdm
from transformers.models.auto.tokenization_auto import AutoTokenizer
from transformers.pipelines import pipeline
from transformers.pipelines.base import Pipeline

from accuracy_eval import calculate_match_frequency
from livedoor_datasets import load_livedoor_corpus


def build_classifier(model_name: str = "Formzu/bert-base-japanese-jsnli") -> Pipeline:
    return pipeline(
        "zero-shot-classification",
        model=model_name,
        tokenizer=AutoTokenizer.from_pretrained(model_name, model_max_length=512),
        device="cuda:0",
        torch_dtype=torch.float16,
    )


def data(texts: list[str]):
    yield from texts


if __name__ == "__main__":
    texts, labels = load_livedoor_corpus()
    candidate_labels = list(set(labels))

    classifier = build_classifier()
    predicts = []
    for result in tqdm(
        classifier(
            data(texts),
            candidate_labels,
            hypothesis_template="この記事のテーマは「{}」です。",
        ),
        total=len(texts),
    ):
        scores = result["scores"]
        likelihood_index = scores.index(max(scores))
        predicts.append(result["labels"][likelihood_index])

    # 精度: 0.424731912583141
    print(f"精度: {calculate_match_frequency(predicts, labels)}")

build_classifier 関数では、transformers.pipeline にタスク名として "zero-shot-classification" を指定し、使用するモデル名などを渡して分類器オブジェクトを構築しています。device="cuda:0"torch_dtype=torch.float16 は、GPUを利用して推論速度を向上させるための設定です。

推論を行う際は、構築した classifier オブジェクトを呼び出し、分類したいテキストのリスト (texts)、候補ラベルのリスト (candidate_labels)、そして重要な hypothesis_template を渡します。hypothesis_template="この記事のテーマは「{}」です。" は、候補ラベル(例:「スポーツ」「ニュース」)が {} の部分に埋め込まれて仮説文が生成されることを示します。pipeline は内部でこれらの仮説文を生成し、元のテキストと組み合わせたNLIペアをモデルに入力し、各ラベルに対するスコア(含意の確率)を計算して返します。

返される結果 result は辞書のリスト形式で、各要素には候補ラベルのリスト ("labels") とそれに対応するスコアのリスト ("scores") が含まれています。私たちは、その中で最もスコアが高いラベルを選んで予測結果 predicts に追加します。

この手法をLivedoorニュースコーパス全体で実行した際の精度は、約 0.425 となりました。NLIベースの手法は、特定のNLIデータでファインチューニングされたモデルの性能に依存しますが、プロンプトの工夫(hypothesis_template の調整など)によって精度が変わる可能性もあります。

手法2:Sentence Transformer による類似度ベース分類

Zero-Shotテキスト分類のもう一つの主要なアプローチは、テキストとラベルの意味的な近さをベクトル空間で捉える方法です。この手法では、「Sentence Transformer」と呼ばれる種類のモデルがよく利用されます。

Sentence Transformerは、単語の埋め込み(Word Embedding)が単語の意味をベクトルで表現するのと同様に、文や段落といったより長いテキストの意味を捉えた固定長のベクトル(Sentence Embedding)を生成することに特化して学習されたモデルです。異なる文であっても、意味的に近い文であれば、生成される埋め込みベクトルもベクトル空間上で近くに配置されるという性質を持ちます。

この性質を利用してZero-Shot分類を行うには、分類したい入力テキストと、候補となる各カテゴリラベル(あるいはその説明文)をそれぞれSentence Embeddingに変換します。そして、入力テキストの埋め込みベクトルと、各ラベルの埋め込みベクトルとの間の「類似度」を計算します。最も類似度が高いラベルを、そのテキストの予測カテゴリとするのです。類似度としては、ベクトルの向きの近さを示すコサイン類似度が一般的によく使われます。

この手法において、特に重要なのは、ラベル名そのものだけでなく、ラベルの説明文などを埋め込み計算に利用することです。抽象的なラベル名(例:「生活」)よりも、具体的な説明文(例:「独身女性の視点から、日々の恋愛、結婚観、仕事の悩み…」)の方が、そのカテゴリがどのような内容を含むのかをモデルがより正確に理解する助けとなり、分類精度が向上する傾向があります。

以下に、Sentence Transformer とコサイン類似度を使ったZero-Shot分類の実装コードを示します。

# embedding_zero_shot.py

import numpy as np
import torch
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
from tqdm import tqdm

from accuracy_eval import calculate_match_frequency
from livedoor_datasets import LABEL_DESCRIPTIONS, load_livedoor_corpus


def build_transformer(
    model_name: str = "paraphrase-multilingual-mpnet-base-v2",
) -> SentenceTransformer:
    return SentenceTransformer(model_name)


if __name__ == "__main__":
    texts, labels = load_livedoor_corpus()
    candidate_labels = list(set(labels))

    model = build_transformer()
    label_embeddings = model.encode([LABEL_DESCRIPTIONS[label] for label in candidate_labels])

    predicts = []
    for text in tqdm(texts):
        text_embedding = model.encode(text)
        if isinstance(text_embedding, torch.Tensor):
            text_embedding = text_embedding.numpy()

        similarities = cosine_similarity(text_embedding.reshape(1, -1), label_embeddings)[0]
        likelihood_index = np.argmax(similarities)
        predicts.append(candidate_labels[likelihood_index])

    # 精度: 0.5630514456359441
    print(f"精度: {calculate_match_frequency(predicts, labels)}")

build_transformer 関数で SentenceTransformer モデルをロードします。ここでは、多くの言語に対応した汎用的なモデルである paraphrase-multilingual-mpnet-base-v2 を使用しています。

推論部分では、まず LABEL_DESCRIPTIONS を使って候補ラベルに対応する説明文のリストを作成し、model.encode() メソッドを使ってまとめて埋め込みベクトル(label_embeddings)を計算しています。次に、各入力テキストに対しても model.encode() で埋め込みベクトル(text_embedding)を計算します。

テキスト埋め込みとラベル埋め込みが得られたら、sklearn.metrics.pairwise.cosine_similarity 関数を用いて、テキスト埋め込み(1つのベクトル)とすべてのラベル埋め込み(複数のベクトル)との間のコサイン類似度を計算します。これにより、入力テキストが各ラベル説明文とどれだけ意味的に類似しているかを示すスコアのリストが得られます。最後に、np.argmax を使って最も類似度が高いラベルのインデックスを見つけ、そのインデックスに対応する候補ラベルを予測結果として採用します。

この手法をLivedoorニュースコーパス全体で実行した際の精度は、約 0.563 となりました。BERT+NLI手法と比較して精度が向上しており、Sentence Transformer がテキスト間の意味的な類似度を捉える能力や、ラベルの説明文を活用するアプローチの有効性が示されています。

手法3:大規模言語モデル (LLM) を利用する

近年目覚ましい発展を遂げている大規模言語モデル(LLM)は、事前のタスク固有の学習なしに、与えられた指示(プロンプト)に基づいて様々なタスクを実行できる高い汎用的なゼロショット能力を持っています。この能力をテキスト分類に応用するアプローチも有効です。

LLMを使ったZero-Shot分類では、分類したいテキストと候補ラベルのリストをプロンプトとしてモデルに与え、「このテキストを以下のカテゴリの中から最も適切なものに分類してください」といった指示を出します。LLMは事前学習で得た膨大な知識と推論能力を使って、指示に従い最適なカテゴリ名を応答として返します。

このプロセスを効率的に行うために、ここでは DSPy というライブラリを利用します。DSPy は、LLMを使った複雑なワークフローを構築するためのフレームワークで、タスクの入出力形式を「シグネチャ」として定義することで、プロンプトエンジニアリングの多くを抽象化してくれます。

今回は、ローカル環境で実行できるオープンソースモデルであるLlama 3.2(Ollama経由)と、OpenAIが提供するAPIモデルであるGPT-4o-miniの2つのLLMバックエンドを使って試してみます。

以下に、DSPy を使用した実装コードを示します。LLMバックエンドの設定部分以外は、Llama 3.2とGPT-4o-miniで共通のコード構造になります。

# llm_zero_shot.py

import time
from typing import Literal

import dspy
from tqdm import tqdm

from accuracy_eval import calculate_match_frequency
from livedoor_datasets import load_livedoor_corpus

lm = dspy.LM("ollama_chat/llama3.2", api_base="http://localhost:11434", api_key="")
# lm = dspy.LM("openai/gpt-4o-mini")
dspy.configure(lm=lm)


class NewsClassifier(dspy.Signature):
    """ニュース記事のカテゴリを分類してください。"""

    sentence: str = dspy.InputField()
    genre: Literal[
        "生活", "IT", "家電", "男性", "映画", "女性", "モバイル", "スポーツ", "ニュース"
    ] = dspy.OutputField()
    confidence: float = dspy.OutputField()


if __name__ == "__main__":
    texts, labels = load_livedoor_corpus()
    candidate_labels = list(set(labels))

    classifier = dspy.Predict(NewsClassifier)

    predicts = []
    for text in tqdm(texts):
        result = classifier(sentence=text)
        predicts.append(result.genre)
   
    # 精度: 0.4392561422560065 (Llama 3.2)
    # 精度: 0.5775756753088096 (gpt-4o-mini)
    print(f"精度: {calculate_match_frequency(predicts, labels)}")

まず、dspy.LM を使って利用するLLMバックエンドを設定し、dspy.configure でdspyに登録します。Ollamaを使う場合はローカルのエンドポイントURLを指定し、OpenAI APIを使う場合はモデル名のみ指定します(APIキーは環境変数などで設定されている前提です)。

次に、dspy.Signature を継承した NewsClassifier クラスで、このタスクの入出力形式を定義します。dspy.InputField で入力(sentence)、dspy.OutputField で出力(genre, confidence)を定義しています。特に、genretyping.Literal[...] を使うことで、モデルに対して出力が指定された9つのカテゴリ名のいずれかであることを強く指示できます。

推論を行う際は、dspy.Predict(NewsClassifier) で作成した予測モジュール classifier を呼び出し、入力フィールド (sentence) にテキストを渡すだけです。dspy が自動的にプロンプトを組み立ててLLMに送信し、応答をパースして、シグネチャで定義した形式(result.genre, result.confidence など)で結果を返してくれます。

このLLMを利用した手法をLivedoorニュースコーパス全体で実行した際の精度は、以下のようになりました。

  • Llama 3.2 (Ollama): 約 0.439
  • GPT-4o-mini (OpenAI API): 約 0.578

Llama 3.2の結果はBERT+NLI手法(約0.425)よりわずかに高いですが、Sentence Transformer手法(約0.563)よりは低い精度となりました。一方、GPT-4o-miniは Sentence Transformer 手法と同等かそれ以上の約 0.578 という最も高い精度を示しました。これは、GPT-4o-miniが持つ高度な言語理解能力と、プロンプトに基づいたZero-Shot推論能力の高さによるものと考えられます。

LLMを使ったZero-Shot分類は、モデルの汎用性が高く、比較的シンプルなプロンプトでも有効な結果が得られる可能性があります。ただし、その性能は使用するLLMモデルに大きく依存します。また、API利用の場合はコストやレイテンシ、ローカル実行の場合は環境構築や必要な計算リソースが考慮事項となります。

各手法の比較と考察

ここまで、3つの異なるアプローチによるZero-Shotテキスト分類を実践し、Livedoorニュースコーパスでの精度を確認しました。それぞれの結果を改めて見てみましょう。

手法使用モデル精度
BERT + NLIFormzu/bert-base-japanese-jsnli約 0.425
Sentence Transformer + 類似度paraphrase-multilingual-mpnet-base-v2 + ラベル説明文約 0.563
LLM (Ollama)Llama 3.2 (量子化)約 0.439
LLM (OpenAI API)GPT-4o-mini約 0.578

この結果からいくつかの興味深い点が分かります。

まず、Sentence Transformer を使用した手法と GPT-4o-mini を使用した手法が、他の手法に比べて高い精度を示しました。これは、これらのモデル(および手法)がLivedoorニュースコーパスのようなニュース記事のカテゴリを、Zero-Shotの状況下でより効果的に識別できたことを示唆しています。

特に興味深いのは、Sentence Transformer 手法(約0.563)と GPT-4o-mini(約0.578)の精度が比較的近かった点です。Sentence Transformer は、テキストとラベル説明文の埋め込みベクトル間の類似度という明確な仕組みで分類を行いました。一方、GPT-4o-mini は汎用的なLLMとして、テキストと候補ラベルをプロンプトとして与えられただけで分類を行いました。

一般的に、BERTのようなモデルは穴埋めタスク(Masked Language Modeling, MLM)などで事前学習されるため、文脈中の単語間の関係性を深く理解する能力に優れています。対照的に、GPTのようなモデルは自己回帰的に次の単語を予測するタスク(Casual Language Modeling)で学習されるため、自然な文章を生成する能力が高い傾向にありますが、必ずしも文章全体の意味や他の概念との関係性を、特定のタスクにおいてBERTほど効率的に捉えるとは限りません。

今回、Sentence Transformer が高い精度を出せたのは、文全体の意味を捉えた埋め込みを生成する能力に加え、特にラベルの説明文という豊富な追加情報を活用できたことが大きく影響していると考えられます。一方、GPT-4o-miniはラベル名のみ(dspyのシグネチャ定義に基づく)で高い精度を達成しており、その基盤となる言語能力の高さがうかがえます。しかし、Sentence Transformerがラベル説明文で引き出した意味的な情報を、GPT-4o-miniはデフォルトのプロンプトでは十分活用しきれていない、あるいは Sentence Transformer のように意味空間での近さを測るアプローチがこのタスクには有効だった、といった側面があるのかもしれません。GPTももちろん意味理解能力は非常に高いですが、事前学習の特性やタスクへのアプローチの違いが、このような結果に繋がった可能性が考えられます。

ローカルで実行したLlama 3.2(量子化モデル)の精度は、Sentence Transformer や GPT-4o-mini よりは低い結果となりました。これは、モデルの規模や品質、量子化による性能低下、そして使用したプロンプト(dspyのデフォルト生成プロンプト)がタスクに完全に最適化されていない可能性などが複合的に影響していると考えられます。APIモデルであるGPT-4o-miniが最も高い精度を示したことは、最新の大規模モデルが持つゼロショット能力の高さを示しています。

より高い精度を出すための改善方法

今回の結果を踏まえ、Zero-Shotテキスト分類においてさらに高い精度を目指すためには、いくつかの改善方法が考えられます。

  1. モデルの選定: 今回試したモデル以外にも、様々な事前学習済みモデルが存在します。日本語に特化したSentence Transformer モデルや、さらに大規模なLLM(GPT-4 Turbo, Claude 3など)、あるいは特定のドメインデータで事前学習されたモデルなどを試すことで、精度が向上する可能性があります。
  2. プロンプトエンジニアリング(LLM向け): LLMを使う場合、プロンプトの質が結果に大きく影響します。ラベルの説明文をプロンプトに含める、タスクの指示をより具体的にする、推論のステップをブレークダウンするよう指示する(Chain-of-Thoughtなど)、少量の代表的な例をプロンプトに含める(これは厳密にはFew-Shotになりますが、データ効率の良い手法として有効です)などの工夫が考えられます。
  3. ラベル情報の活用: ラベル名だけでなく、LABEL_DESCRIPTIONS のようなラベルに関する詳細な説明文を最大限に活用するアプローチは有効です。説明文をより詳細にしたり、ラベル間の階層構造や関連性といったオントロジー情報を利用したりすることも、分類のヒントになり得ます。Sentence Transformer 手法で精度が高かったのは、このラベル説明文の利用が奏功したと考えられます。
  4. 手法の組み合わせ(アンサンブル): 複数の異なるZero-Shot分類手法やモデルによる予測結果を組み合わせることで、単一モデルの弱点を補い、より安定した高い精度を得られる場合があります。例えば、各手法の予測確率を平均する、多数決を取る、といった方法が考えられます。
  5. Few-Shot学習など: 厳密にはZero-Shotではなくなりますが、もし分類対象のドメインで少量のラベル付きデータを用意できるのであれば、事前学習済みモデルをそのデータで追加学習(ファインチューニング)することで、Zero-Shotよりも大幅な精度向上が期待できます。

これらの改善方法を組み合わせることで、タスクやデータセットの特性に合わせて最適なZero-Shot分類システムを構築することが可能になります。

おわりに

この記事では、Zero-Shotテキスト分類の基本的な考え方から、実際のデータセットを使った実践的な手法まで解説しました。ラベル付きデータが限られる、あるいは全くない状況でもテキスト分類を可能にするZero-Shot手法が、データ活用の幅を大きく広げる強力なアプローチであることがお分かりいただけたかと思います。

Livedoorニュースコーパスを例に、BERTとNLIを組み合わせる方法、Sentence Transformerで埋め込み類似度を計算する方法、そして最新のLLMを活用する方法という、異なる特徴を持つ3つの主要なアプローチを比較しました。それぞれにメリット・デメリットがあり、タスクの性質や利用可能なリソースに応じて最適な手法を選択したり、組み合わせて活用したりすることが重要です。

Zero-Shotテキスト分類の技術は日々進化しており、今後さらに高精度かつ汎用的なモデルが登場することが期待されます。未知のカテゴリへの対応やデータ準備コスト削減の観点から、この技術の重要性はますます高まっていくでしょう。

今回ご紹介したコードは、Zero-Shotテキスト分類を始めるための第一歩としてご活用いただけます。ぜひ実際に手を動かして、様々なモデルやデータに対して試し、その可能性を体感してみてください。

More Information