高度なテキスト分類: マルチラベル、ゼロショットおよびフューショット
La テキストの分類 最も一般的な NLP タスクの 1 つですが、実際には それは単に「ポジティブかネガティブか」ということをはるかに超えています。ニュース記事も同じかもしれない 政治、経済、国際的な時間。サポート チケットは複数に属することができます カテゴリを同時に実行します。文書を見なくても機密化できる トレーニング中のそのカテゴリの例。
この記事では、テキスト分類の複雑さをすべて取り上げます。 マルチクラス (相互に排他的な複数のクラス)、マルチラベル (複数の同時ラベル)、 階層的分類、およびゼロショット分類 (トレーニング例なし) 対象クラスの場合)。イタリアのデータセット、Focal Loss を使用した実装が含まれています 不均衡なデータセットと完全な運用パイプラインに対応します。
これはシリーズの 6 番目の記事です 最新の NLP: BERT から LLM へ。 BERT と HuggingFace エコシステムに精通していることを前提としています。
何を学ぶか
- バイナリ分類、マルチクラス分類、マルチラベル分類の違い - いつ何を使用するか
- ソフトマックスおよび複合メトリクスを使用したマルチクラス分類のための BERT 微調整
- シグモイド、BCEWithLogitsLoss、Focal Loss を使用したマルチラベル分類
- F1 最適化によるマルチラベルでのラベルごとのしきい値調整
- NLI モデル (BART、DeBERTa-v3) とカスタム テンプレートによるゼロショット分類
- SetFit による少数ショット分類: クラスあたり 8 ~ 64 個の例
- フラットとトップダウンによる階層分類
- 不均衡なデータセットの管理: クラスの重み付け、焦点損失、オーバーサンプリング
- マルチラベルメトリクス: ハミング損失、ミクロ/マクロ F1、サブセット精度
- キャッシュとバッチ推論を備えた実稼働対応の分類パイプライン
1. テキスト分類分類法
正しい分類の種類を選択することが重要な最初のステップです。 この選択は、損失関数、評価指標、モデル アーキテクチャに影響を与えます。
テキスト分類の種類: 選択ガイド
| タイプ | 説明 | 実践例 | 出力層 | 損失関数 | 主要な指標 |
|---|---|---|---|---|---|
| バイナリ | 2 つの相互排他的なクラス | スパム vs ハム、ポジティブ vs ネガティブ | シグモイド(1) | BCEロジッツ損失あり | F1、AUC-ROC |
| マルチクラス | N クラス、選択肢 1 | ニュースカテゴリ、テキスト言語 | ソフトマックス(N) | クロスエントロピー損失 | 精度、F1マクロ |
| マルチラベル | N クラス、複数のクラスが同時にアクティブ | 記事タグ、複数の感情 | シグモイド(N) | BCEロジッツ損失あり | ハミングロス、マイクロF1 |
| 階層的 | 階層的に編成されたクラス | 製品カテゴリ (エレクトロニクス > テレビ > OLED) | 場合によって異なります | 階層的損失 | レベル別 F1 |
| ゼロショット | トレーニング中に見たことのないクラス | 任意のトピックでのルーティング | NLI 含意スコア | NLI トレーニングの損失 | F1、クラスごとの精度 |
| 少数ショット (SetFit) | クラスあたりの例が少ない (8 ~ 64) | ドメイン固有の分類 | 物流責任者 | コントラスト + CE | 精度、F1 |
2. BERTによるマルチクラス分類
マルチクラスの場合、各例で正しいクラスは 1 つだけです。 使ってみましょう ソフトマックス 出力層の活性化関数として e クロスエントロピー損失 損失関数として。 BERT は、AG News (~95%)、yelp-full (~70%)、 Yahoo Answers (~77%)。
from transformers import (
AutoModelForSequenceClassification,
AutoTokenizer,
TrainingArguments,
Trainer,
EarlyStoppingCallback
)
from datasets import load_dataset
import evaluate
import numpy as np
import torch
# AG News: classifica notizie in 4 categorie (dataset bilanciato)
# World, Sports, Business, Sci/Tech
dataset = load_dataset("ag_news")
print("Dataset AG News:", dataset)
# train: 120,000 esempi (30,000 per classe)
# test: 7,600 esempi
LABELS = ["World", "Sports", "Business", "Sci/Tech"]
num_labels = len(LABELS)
MODEL = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(MODEL)
def tokenize(examples):
return tokenizer(
examples["text"],
truncation=True,
padding="max_length",
max_length=128
)
tokenized = dataset.map(tokenize, batched=True, remove_columns=["text"])
tokenized.set_format("torch")
# Modello multi-class: softmax su 4 classi
model = AutoModelForSequenceClassification.from_pretrained(
MODEL,
num_labels=num_labels,
id2label={i: l for i, l in enumerate(LABELS)},
label2id={l: i for i, l in enumerate(LABELS)}
)
# Metriche composite per classificazione multi-class
accuracy = evaluate.load("accuracy")
f1 = evaluate.load("f1")
def compute_metrics(eval_pred):
logits, labels = eval_pred
preds = np.argmax(logits, axis=-1)
# Softmax per probabilità
probs = torch.softmax(torch.tensor(logits, dtype=torch.float32), dim=-1).numpy()
max_prob = probs.max(axis=1).mean() # confidenza media
return {
"accuracy": accuracy.compute(predictions=preds, references=labels)["accuracy"],
"f1_macro": f1.compute(predictions=preds, references=labels, average="macro")["f1"],
"f1_weighted": f1.compute(predictions=preds, references=labels, average="weighted")["f1"],
"avg_confidence": float(max_prob)
}
args = TrainingArguments(
output_dir="./results/bert-agnews",
num_train_epochs=3,
per_device_train_batch_size=32,
per_device_eval_batch_size=64,
learning_rate=2e-5,
warmup_ratio=0.1,
weight_decay=0.01,
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="f1_macro",
fp16=True,
report_to="none",
seed=42
)
trainer = Trainer(
model=model, args=args,
train_dataset=tokenized["train"],
eval_dataset=tokenized["test"],
compute_metrics=compute_metrics,
callbacks=[EarlyStoppingCallback(early_stopping_patience=2)]
)
trainer.train()
# Risultati attesi su AG News test:
# Accuracy: ~94-95%, F1 macro: ~94-95%
# Inferenza production-ready
from transformers import pipeline
clf_pipeline = pipeline(
"text-classification",
model=model,
tokenizer=tokenizer,
device=0 if torch.cuda.is_available() else -1
)
texts = [
"European Central Bank raises interest rates by 0.5 percentage points",
"Juventus wins Champions League final against Real Madrid"
]
predictions = clf_pipeline(texts)
for text, pred in zip(texts, predictions):
print(f" '{text[:50]}...' -> {pred['label']} ({pred['score']:.3f})")
3. マルチラベル分類
マルチラベルでは、各サンプルに 0 個、1 個、または複数のアクティブなラベルを含めることができます。 マルチクラスと比較した根本的な変更は出力層にあります (ソフトマックスの代わりにシグモイド) および損失関数 (CrossEntropyLoss の代わりに BCEWithLogitsLoss)。 各クラスは独立した二項問題として扱われます。
3.1 マルチラベルデータセットの準備
from datasets import Dataset
from transformers import AutoTokenizer
import torch
import numpy as np
# Dataset multi-label: articoli di news con tag multipli
data = {
"text": [
"La BCE alza i tassi di interesse per combattere l'inflazione europea",
"La Juventus batte il Milan 2-1 in una partita emozionante al Bernabeu",
"Apple presenta il nuovo iPhone con chip M4 e AI generativa avanzata",
"Il governo italiano approva la nuova legge fiscale tra polemiche politiche",
"La crisi climatica colpisce le economie dei paesi in via di sviluppo",
"Tesla annuncia nuovo stabilimento in Italia con 2000 posti di lavoro",
"La Commissione Europea propone nuove regole sull'intelligenza artificiale",
],
# Label: [economia, politica, sport, tecnologia, ambiente, italia]
"labels": [
[1, 0, 0, 0, 0, 0], # solo economia
[0, 0, 1, 0, 0, 0], # solo sport
[0, 0, 0, 1, 0, 0], # solo tecnologia
[1, 1, 0, 0, 0, 1], # economia + politica + italia
[1, 0, 0, 0, 1, 0], # economia + ambiente
[1, 0, 0, 1, 0, 1], # economia + tecnologia + italia
[1, 1, 0, 1, 0, 0], # economia + politica + tecnologia
]
}
LABELS = ["economia", "politica", "sport", "tecnologia", "ambiente", "italia"]
NUM_LABELS = len(LABELS)
tokenizer = AutoTokenizer.from_pretrained("bert-base-multilingual-cased")
def tokenize_multilabel(examples):
encoding = tokenizer(
examples["text"],
truncation=True,
padding="max_length",
max_length=128
)
# Converti labels in float (richiesto da BCEWithLogitsLoss)
encoding["labels"] = [
[float(l) for l in label_list]
for label_list in examples["labels"]
]
return encoding
dataset = Dataset.from_dict(data)
tokenized = dataset.map(tokenize_multilabel, batched=True, remove_columns=["text"])
tokenized.set_format("torch", columns=["input_ids", "attention_mask", "token_type_ids"])
# Analisi della distribuzione delle label
import pandas as pd
labels_df = pd.DataFrame(data["labels"], columns=LABELS)
print("\nDistribuzione label:")
for col in LABELS:
count = labels_df[col].sum()
print(f" {col}: {count}/{len(labels_df)} esempi ({100*count/len(labels_df):.0f}%)")
3.2 カスタム損失を備えたマルチラベル モデル
from transformers import AutoModelForSequenceClassification, Trainer, TrainingArguments
from torch import nn
import torch
import numpy as np
# =========================================
# Trainer con BCEWithLogitsLoss standard
# =========================================
class MultiLabelTrainer(Trainer):
"""Trainer personalizzato per multi-label con BCEWithLogitsLoss."""
def compute_loss(self, model, inputs, return_outputs=False):
labels = inputs.pop("labels")
outputs = model(**inputs)
logits = outputs.logits
# BCEWithLogitsLoss per multi-label
# Combina sigmoid + BCE in un'unica operazione numericamente stabile
loss_fct = nn.BCEWithLogitsLoss()
loss = loss_fct(logits.float(), labels.float().to(logits.device))
return (loss, outputs) if return_outputs else loss
# =========================================
# Focal Loss per dataset sbilanciati
# =========================================
class FocalLossMultiLabelTrainer(Trainer):
"""
Trainer con Focal Loss per gestire dataset multi-label sbilanciati.
Focal Loss riduce il peso degli esempi facili (ben classificati)
e aumenta l'attenzione sugli esempi difficili.
FL(p_t) = -alpha_t * (1 - p_t)^gamma * log(p_t)
gamma=2 e il valore standard (Lin et al. 2017)
"""
def __init__(self, *args, focal_gamma: float = 2.0, focal_alpha: float = 0.25, **kwargs):
super().__init__(*args, **kwargs)
self.focal_gamma = focal_gamma
self.focal_alpha = focal_alpha
def compute_loss(self, model, inputs, return_outputs=False):
labels = inputs.pop("labels")
outputs = model(**inputs)
logits = outputs.logits
# Focal Loss implementation
probs = torch.sigmoid(logits.float())
labels_float = labels.float().to(logits.device)
# Standard BCE term
bce_loss = nn.functional.binary_cross_entropy_with_logits(
logits.float(),
labels_float,
reduction='none'
)
# Focal modulation
pt = probs * labels_float + (1 - probs) * (1 - labels_float)
focal_weight = (1 - pt) ** self.focal_gamma
# Alpha balancing (peso diverso per classe positiva vs negativa)
alpha_t = self.focal_alpha * labels_float + (1 - self.focal_alpha) * (1 - labels_float)
focal_loss = (alpha_t * focal_weight * bce_loss).mean()
return (focal_loss, outputs) if return_outputs else focal_loss
model = AutoModelForSequenceClassification.from_pretrained(
"bert-base-multilingual-cased",
num_labels=NUM_LABELS,
problem_type="multi_label_classification"
)
# Metriche multi-label
def compute_multilabel_metrics(eval_pred):
logits, labels = eval_pred
probs = torch.sigmoid(torch.tensor(logits)).numpy()
predictions = (probs >= 0.5).astype(int)
# Hamming Loss: percentuale di label sbagliate (lower is better)
hamming = np.mean(predictions != labels)
# Subset accuracy: % di esempi con TUTTE le label corrette
exact_match = np.mean(np.all(predictions == labels, axis=1))
from sklearn.metrics import f1_score
micro_f1 = f1_score(labels, predictions, average='micro', zero_division=0)
macro_f1 = f1_score(labels, predictions, average='macro', zero_division=0)
return {
"hamming_loss": hamming,
"subset_accuracy": exact_match,
"micro_f1": micro_f1,
"macro_f1": macro_f1
}
args = TrainingArguments(
output_dir="./results/bert-multilabel",
num_train_epochs=5,
per_device_train_batch_size=16,
learning_rate=2e-5,
warmup_ratio=0.1,
fp16=True,
report_to="none"
)
# Usa FocalLoss per dataset sbilanciati, MultiLabelTrainer per bilanciati
trainer = FocalLossMultiLabelTrainer(
model=model, args=args,
train_dataset=tokenized,
compute_metrics=compute_multilabel_metrics,
focal_gamma=2.0,
focal_alpha=0.25
)
trainer.train()
3.3 ラベルのしきい値の最適化
デフォルトのしきい値 (0.5) は、マルチラベルのすべてのラベルにとって常に最適であるとは限りません。 F1 を最大化するために、各ラベルのしきい値を個別に最適化することが可能です。 これは、不均衡なデータセットの場合に特に重要です。
from sklearn.metrics import f1_score
import numpy as np
import torch
def find_optimal_thresholds(logits: np.ndarray, true_labels: np.ndarray,
thresholds=None, label_names=None) -> np.ndarray:
"""
Trova il threshold ottimale per ogni label che massimizza l'F1.
Usa il validation set per la ricerca del threshold.
"""
if thresholds is None:
thresholds = np.arange(0.05, 0.95, 0.05)
probs = 1 / (1 + np.exp(-logits)) # sigmoid
n_labels = logits.shape[1]
optimal_thresholds = np.zeros(n_labels)
print("Ricerca threshold ottimale per label:")
for label_idx in range(n_labels):
best_f1 = 0
best_threshold = 0.5
for threshold in thresholds:
preds = (probs[:, label_idx] >= threshold).astype(int)
f1 = f1_score(true_labels[:, label_idx], preds, zero_division=0)
if f1 > best_f1:
best_f1 = f1
best_threshold = threshold
optimal_thresholds[label_idx] = best_threshold
label_name = label_names[label_idx] if label_names else f"label_{label_idx}"
support = true_labels[:, label_idx].sum()
print(f" {label_name:<15s}: threshold={best_threshold:.2f}, F1={best_f1:.4f} (n={support})")
return optimal_thresholds
# Funzione di predict con thresholds personalizzati
def predict_multilabel(
texts: list,
model,
tokenizer,
thresholds: np.ndarray,
label_names: list,
batch_size: int = 32
) -> list:
"""Predizione multi-label con thresholds per-label ottimizzati."""
model.eval()
all_results = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i+batch_size]
inputs = tokenizer(batch, return_tensors='pt', truncation=True,
padding=True, max_length=128)
with torch.no_grad():
logits = model(**inputs).logits
probs = torch.sigmoid(logits).numpy()
for sample_probs in probs:
sample_results = []
for j, (prob, threshold, label) in enumerate(
zip(sample_probs, thresholds, label_names)):
if prob >= threshold:
sample_results.append({"label": label, "probability": float(prob)})
all_results.append(sorted(sample_results, key=lambda x: x["probability"], reverse=True))
return all_results
# Esempio di utilizzo
# val_logits, val_labels = get_val_predictions(trainer, val_dataset)
# thresholds = find_optimal_thresholds(val_logits, val_labels, label_names=LABELS)
# predictions = predict_multilabel(texts, model, tokenizer, thresholds, LABELS)
4. ゼロショットの分類
La ゼロショット分類 テキストをカテゴリに分類できます モデルがトレーニング中に見たことがないもの。トレーニングされたモデルを活用する 自然言語推論 (NLI): テキストと仮説が与えられた場合、 モデルは、仮説が真 (含意)、偽 (矛盾)、または 不確実(中立)。
プロセス: テキストは「前提」として使用され、カテゴリは「仮説」として使用されます。 (例:「このテキストは経済学についてです」)。含意スコアは次のことを示します テキストがそのカテゴリにどの程度属しているか。
from transformers import pipeline
# Modelli NLI consigliati per zero-shot:
# - facebook/bart-large-mnli (inglese, ottimo per EN)
# - cross-encoder/nli-deberta-v3-large (più accurato, EN)
# - MoritzLaurer/mDeBERTa-v3-base-mnli-xnli (multilingua, include IT)
# - joeddav/xlm-roberta-large-xnli (multilingua alternativo)
# Zero-shot per italiano (multilingue)
classifier_it = pipeline(
"zero-shot-classification",
model="MoritzLaurer/mDeBERTa-v3-base-mnli-xnli",
device=0 # usa GPU se disponibile
)
# Classificazione di un articolo italiano
text_it = "La BCE ha alzato i tassi di interesse di 25 punti base nella riunione di ottobre."
categories_it = ["economia", "politica", "sport", "tecnologia", "ambiente"]
result = classifier_it(
text_it,
candidate_labels=categories_it,
multi_label=False # True per multi-label simultaneo
)
print("Classificazione testo IT:")
for label, score in zip(result['labels'][:3], result['scores'][:3]):
print(f" {label}: {score:.3f}")
# Zero-shot multi-label
text_multi = "Tesla investe 2 miliardi in pannelli solari riducendo le emissioni CO2."
result_multi = classifier_it(
text_multi,
candidate_labels=categories_it,
multi_label=True
)
print("\nClassificazione multi-label:")
for label, score in zip(result_multi['labels'], result_multi['scores']):
if score > 0.3:
print(f" {label}: {score:.3f}")
# =========================================
# Template personalizzati per migliori risultati
# =========================================
classifier_en = pipeline("zero-shot-classification", model="facebook/bart-large-mnli")
# Default: "This example is {label}."
# Personalizzato: più descrittivo e preciso
text = "The Federal Reserve raised interest rates by 50 basis points."
# Confronto template
templates = {
"default": "This example is {}.",
"topic_specific": "This news article is about {}.",
"domain_specific": "This text is related to {} matters.",
}
for template_name, template in templates.items():
result = classifier_en(
text,
candidate_labels=["economics", "politics", "sports"],
hypothesis_template=template
)
print(f"\nTemplate '{template_name}': top={result['labels'][0]} ({result['scores'][0]:.3f})")
# Template per dominio legale
legal_text = "The court ruled in favor of the plaintiff in the patent infringement case."
legal_result = classifier_en(
legal_text,
candidate_labels=["intellectual property", "criminal law", "employment law"],
hypothesis_template="This legal document concerns {} law."
)
print(f"\nDominio legale: {legal_result['labels'][0]} ({legal_result['scores'][0]:.3f})")
5. SetFit による少数ショットの分類
SetFit (文章変換微調整) そしてそれを可能にする技術 クラスごとに非常に少ないサンプル (8 ~ 16 個のサンプル) を使用して正確な分類器をトレーニングします。 アイデアはシンプルです。まず、文変換器をトレーニングしてペアを認識します。 少数のショットのデータセットを使用して類似/非類似を確認し、単純な頭部をトレーニングします 結果の埋め込みに対するロジスティック分類の。
SetFit は、多くのベンチマークで標準の微調整や GPT-3 の少数ショットよりも優れたパフォーマンスを発揮します クラスごとに 8 つの例があり、はるかに小さいモデルを使用します。
# pip install setfit
from setfit import SetFitModel, Trainer as SetFitTrainer, TrainingArguments as SetFitArgs
from datasets import Dataset, DatasetDict
import pandas as pd
# Dataset italiano few-shot: solo 8 esempi per classe
train_data = {
"text": [
# Economia (8 esempi)
"I tassi di interesse BCE aumentati dello 0.5%",
"Il PIL italiano cresce dell'1.2% nel terzo trimestre",
"La Borsa di Milano chiude in rialzo del 2.3%",
"L'inflazione scende al 2.8% grazie al calo dei prezzi",
"Fiat annuncia 3000 nuove assunzioni in Piemonte",
"Il deficit italiano supera il 3% del PIL europeo",
"Le esportazioni crescono verso i mercati emergenti",
"L'euro si rafforza rispetto al dollaro americano",
# Sport (8 esempi)
"La Juventus conquista la Coppa Italia ai rigori",
"Jannik Sinner vince il titolo ATP Finals a Torino",
"Ferrari ottiene la pole position a Silverstone",
"La nazionale azzurra batte la Germania 3-1",
"Il Milan acquista un attaccante per 80 milioni",
"La Roma pareggia 2-2 con l'Inter nel posticipo",
"Gianmarco Tamberi difende il titolo europeo nel salto in alto",
"La pallavolo italiana vince il Mondiale femminile",
# Tecnologia (8 esempi)
"OpenAI lancia il nuovo modello GPT con capacità multimodali",
"Apple presenta la nuova serie iPhone con chip 4nm",
"Google acquisisce una startup di intelligenza artificiale",
"Tesla aumenta la produzione di veicoli elettrici del 40%",
"Meta introduce nuovi filtri di privacy per gli utenti",
"Samsung annuncia chip con tecnologia 2nm nel 2025",
"Microsoft integra Copilot AI in Windows 12",
"Il 5G copre ora il 70% della popolazione italiana",
],
"label": [
0, 0, 0, 0, 0, 0, 0, 0, # economia = 0
1, 1, 1, 1, 1, 1, 1, 1, # sport = 1
2, 2, 2, 2, 2, 2, 2, 2 # tecnologia = 2
]
}
# Dataset di test più grande
test_data = {
"text": [
"La BCE mantiene invariati i tassi al 4.5%",
"L'Inter batte il Liverpool in Champions League",
"NVIDIA supera i 2 trilioni di capitalizzazione",
"Il governo italiano approva il Piano Mattei per l'Africa",
],
"label": [0, 1, 2, 0]
}
train_dataset = Dataset.from_dict(train_data)
test_dataset = Dataset.from_dict(test_data)
# Carica modello SetFit multilingue
model = SetFitModel.from_pretrained(
"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
labels=["economia", "sport", "tecnologia"]
)
# Training con SetFit
args = SetFitArgs(
batch_size=16,
num_epochs=1, # epoche per la testa di classificazione
num_iterations=20, # numero di coppie contrastive generate
eval_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
report_to="none",
)
trainer = SetFitTrainer(
model=model,
args=args,
train_dataset=train_dataset,
eval_dataset=test_dataset,
metric="accuracy"
)
trainer.train()
# Inferenza
texts = [
"Il governo ha approvato la manovra finanziaria 2025",
"L'Inter ha battuto il Barcellona 3-1 in Champions League",
"OpenAI presenta il nuovo modello reasoning o3-pro"
]
predictions = model.predict(texts)
scores = model.predict_proba(texts)
print("\nPredizioni SetFit:")
for text, pred, prob in zip(texts, predictions, scores):
label_names = ["economia", "sport", "tecnologia"]
print(f" '{text[:45]}...' -> {label_names[pred]} ({max(prob):.3f})")
6. 階層的分類
現実世界の多くのシナリオでは、カテゴリは階層に編成されます。 記事は「テクノロジー > AI > NLP」に分類できます。 主なアプローチは 2 つあります。 フラット (階層を無視) e 階層的な (構造を利用してください)。
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import torch
from typing import Dict, List, Tuple
# Esempio di gerarchia di categorie
HIERARCHY = {
"Economia": ["Mercati Finanziari", "Macroeconomia", "Commercio", "Lavoro"],
"Politica": ["Politica Nazionale", "Politica Estera", "Elezioni", "Legislazione"],
"Sport": ["Calcio", "Tennis", "Formula 1", "Atletica"],
"Tecnologia": ["AI", "Smartphone", "Cloud", "Cybersecurity"]
}
class HierarchicalClassifier:
"""
Classificatore gerarchico top-down.
Step 1: classifica nella categoria di primo livello
Step 2: classifica nella sottocategoria (secondo livello)
"""
def __init__(self, coarse_model_path: str, fine_models: Dict[str, str]):
"""
coarse_model: modella categorie di alto livello (Economia, Politica, ...)
fine_models: dict {coarse_category -> fine_model_path}
"""
self.tokenizer = AutoTokenizer.from_pretrained(coarse_model_path)
self.coarse_model = AutoModelForSequenceClassification.from_pretrained(coarse_model_path)
self.coarse_model.eval()
self.fine_models = {}
for cat, path in fine_models.items():
m = AutoModelForSequenceClassification.from_pretrained(path)
m.eval()
self.fine_models[cat] = m
def predict(self, text: str, return_scores: bool = False) -> Tuple[str, str, float]:
"""
Classifica in modo gerarchico.
Returns: (coarse_label, fine_label, confidence)
"""
inputs = self.tokenizer(text, return_tensors='pt', truncation=True, max_length=256)
# Step 1: classificazione coarse
with torch.no_grad():
coarse_logits = self.coarse_model(**inputs).logits
coarse_probs = torch.softmax(coarse_logits, dim=-1)[0]
coarse_id = coarse_probs.argmax().item()
coarse_label = self.coarse_model.config.id2label[coarse_id]
coarse_score = coarse_probs[coarse_id].item()
# Step 2: classificazione fine (se disponibile per questa categoria)
fine_label = None
fine_score = None
if coarse_label in self.fine_models:
with torch.no_grad():
fine_logits = self.fine_models[coarse_label](**inputs).logits
fine_probs = torch.softmax(fine_logits, dim=-1)[0]
fine_id = fine_probs.argmax().item()
fine_label = self.fine_models[coarse_label].config.id2label[fine_id]
fine_score = fine_probs[fine_id].item()
return {
"coarse": coarse_label,
"fine": fine_label,
"coarse_confidence": coarse_score,
"fine_confidence": fine_score,
"full_path": f"{coarse_label} > {fine_label}" if fine_label else coarse_label
}
print("HierarchicalClassifier definito!")
7. マルチラベルの包括的な指標
from sklearn.metrics import (
f1_score, precision_score, recall_score,
hamming_loss, accuracy_score, average_precision_score
)
import numpy as np
def multilabel_evaluation_report(y_true: np.ndarray, y_pred: np.ndarray,
y_proba: np.ndarray, label_names: list) -> dict:
"""Report completo per classificazione multi-label."""
print("=" * 65)
print("MULTI-LABEL CLASSIFICATION REPORT")
print("=" * 65)
# Metriche globali
hl = hamming_loss(y_true, y_pred)
sa = accuracy_score(y_true, y_pred) # subset accuracy
micro_f1 = f1_score(y_true, y_pred, average='micro', zero_division=0)
macro_f1 = f1_score(y_true, y_pred, average='macro', zero_division=0)
print(f"\n{'Hamming Loss':<25s}: {hl:.4f} (lower is better)")
print(f"{'Subset Accuracy':<25s}: {sa:.4f} (all labels must match)")
print(f"{'Micro F1':<25s}: {micro_f1:.4f} (label-weighted)")
print(f"{'Macro F1':<25s}: {macro_f1:.4f} (unweighted)")
print(f"{'Weighted F1':<25s}: {f1_score(y_true, y_pred, average='weighted', zero_division=0):.4f}")
# AUC per label (richiede probabilità, non predizioni binarie)
if y_proba is not None:
try:
macro_auc = average_precision_score(y_true, y_proba, average='macro')
print(f"{'Macro AP (AUC)':<25s}: {macro_auc:.4f}")
except Exception:
pass
# Per ogni label
print("\nPer-label metrics:")
header = f"{'Label':<18s} {'Precision':>10s} {'Recall':>10s} {'F1':>8s} {'Support':>10s}"
print(header)
print("-" * 65)
for i, label in enumerate(label_names):
prec = precision_score(y_true[:, i], y_pred[:, i], zero_division=0)
rec = recall_score(y_true[:, i], y_pred[:, i], zero_division=0)
f1 = f1_score(y_true[:, i], y_pred[:, i], zero_division=0)
support = int(y_true[:, i].sum())
print(f"{label:<18s} {prec:>10.4f} {rec:>10.4f} {f1:>8.4f} {support:>10d}")
return {
"hamming_loss": hl, "subset_accuracy": sa,
"micro_f1": micro_f1, "macro_f1": macro_f1
}
8. 本番環境に対応した分類パイプライン
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import torch
import numpy as np
from functools import lru_cache
from typing import Union, List, Dict
import time
class ProductionClassifier:
"""
Classificatore production-ready con:
- Caching degli input tokenizzati
- Batch inference per efficienza
- Supporto multi-class e multi-label
- Monitoraggio latenza e confidenza
"""
def __init__(
self,
model_path: str,
task: str = "multi_class", # "multi_class" o "multi_label"
thresholds: np.ndarray = None,
max_length: int = 128,
batch_size: int = 32
):
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.tokenizer = AutoTokenizer.from_pretrained(model_path)
self.model = AutoModelForSequenceClassification.from_pretrained(model_path)
self.model.to(self.device)
self.model.eval()
self.task = task
self.label_names = list(self.model.config.id2label.values())
self.thresholds = thresholds if thresholds is not None else np.full(len(self.label_names), 0.5)
self.max_length = max_length
self.batch_size = batch_size
self._latencies = []
@torch.no_grad()
def predict(self, texts: Union[str, List[str]]) -> List[Dict]:
"""Predizione con monitoraggio latenza."""
if isinstance(texts, str):
texts = [texts]
start = time.perf_counter()
all_results = []
for i in range(0, len(texts), self.batch_size):
batch = texts[i:i+self.batch_size]
inputs = self.tokenizer(
batch,
return_tensors='pt',
truncation=True,
padding=True,
max_length=self.max_length
).to(self.device)
outputs = self.model(**inputs)
logits = outputs.logits.cpu().numpy()
if self.task == "multi_class":
probs = np.exp(logits) / np.exp(logits).sum(axis=1, keepdims=True)
for p in probs:
pred_id = p.argmax()
all_results.append({
"label": self.label_names[pred_id],
"score": float(p[pred_id]),
"all_scores": {name: float(score) for name, score in zip(self.label_names, p)}
})
else: # multi_label
probs = 1 / (1 + np.exp(-logits))
for p in probs:
labels = [
{"label": name, "score": float(score)}
for name, score, thr in zip(self.label_names, p, self.thresholds)
if score >= thr
]
all_results.append({"labels": sorted(labels, key=lambda x: -x["score"])})
latency_ms = (time.perf_counter() - start) * 1000
self._latencies.append(latency_ms)
return all_results
def get_stats(self) -> Dict:
"""Statistiche sulle latenze di inference."""
if not self._latencies:
return {}
return {
"avg_latency_ms": np.mean(self._latencies),
"p99_latency_ms": np.percentile(self._latencies, 99),
"total_predictions": len(self._latencies)
}
# Esempio di utilizzo
# clf = ProductionClassifier("./models/my-classifier", task="multi_label")
# results = clf.predict(["Il governo alza le tasse", "La Juve vince lo scudetto"])
# stats = clf.get_stats()
print("ProductionClassifier definito!")
9. 生成モデルによる分類 (LLM プロンプト)
LLM の出現により、テキスト分類を実行できるようになりました。 トレーニングなしで、ただ促すだけで。このアプローチと ラピッドプロトタイピングや新しいカテゴリまたは珍しいカテゴリに特に役立ちます。
from transformers import pipeline
import json
# =========================================
# Classificazione con LLM tramite prompting
# =========================================
# Approccio 1: Con un modello instruction-following (es. Mistral-7B-Instruct)
def classify_with_llm(text: str, categories: list, model_pipeline) -> dict:
"""
Classificazione zero-shot con LLM instruction-following.
Il modello non richiede fine-tuning: usa la comprensione del linguaggio naturale.
"""
categories_str = ", ".join(categories)
prompt = f"""Classifica il seguente testo in UNA delle categorie: {categories_str}.
Testo: "{text}"
Rispondi SOLO con il nome della categoria, senza spiegazioni.
Categoria:"""
response = model_pipeline(
prompt,
max_new_tokens=20,
temperature=0.0, # deterministic
do_sample=False
)[0]['generated_text']
# Estrai la categoria dalla risposta
answer = response[len(prompt):].strip().split('\n')[0].strip()
# Valida che la risposta sia una categoria valida
for cat in categories:
if cat.lower() in answer.lower():
return {"label": cat, "method": "llm", "raw_answer": answer}
return {"label": "sconosciuto", "method": "llm", "raw_answer": answer}
# Approccio 2: Prompt con esempi (few-shot)
def classify_with_fewshot(text: str, categories: list, examples: list, model_pipeline) -> dict:
"""
Classificazione few-shot: fornisce esempi nel prompt per guidare il modello.
"""
examples_str = ""
for ex in examples[:3]: # massimo 3 esempi per non superare il context window
examples_str += f'Testo: "{ex["text"]}"\nCategoria: {ex["label"]}\n\n'
prompt = f"""Classifica testi nelle categorie: {", ".join(categories)}.
Esempi:
{examples_str}Testo: "{text}"
Categoria:"""
response = model_pipeline(
prompt, max_new_tokens=15, temperature=0.0, do_sample=False
)[0]['generated_text']
answer = response[len(prompt):].strip().split('\n')[0].strip()
return {"label": answer, "method": "few-shot-llm"}
# =========================================
# Confronto: zero-shot NLI vs LLM prompting vs BERT fine-tuned
# =========================================
comparison_table = [
{"method": "BERT fine-tuned", "F1": "0.95+", "speed": "veloce", "dati": "1000+ esempi", "costo": "basso"},
{"method": "SetFit (few-shot)", "F1": "0.85+", "speed": "veloce", "dati": "8-64 esempi", "costo": "basso"},
{"method": "Zero-shot NLI", "F1": "0.70+", "speed": "medio", "dati": "zero esempi", "costo": "basso"},
{"method": "LLM prompting (7B)", "F1": "0.75+", "speed": "lento", "dati": "zero esempi", "costo": "medio"},
{"method": "LLM few-shot (7B)", "F1": "0.82+", "speed": "lento", "dati": "3-10 esempi", "costo": "medio"},
{"method": "GPT-4 prompting (API)", "F1": "0.88+", "speed": "molto lento", "dati": "zero esempi", "costo": "alto"},
]
print("=== Confronto Metodi di Classificazione ===")
print(f"{'Metodo':<30s} {'F1':<10s} {'Velocita':<15s} {'Dati Richiesti':<18s} {'Costo'}")
print("-" * 85)
for row in comparison_table:
print(f"{row['method']:<30s} {row['F1']:<10s} {row['speed']:<15s} {row['dati']:<18s} {row['costo']}")
print("\nRaccomandazione: inizia con zero-shot NLI per validare il task,")
print("poi fine-tuna BERT se hai dati, oppure usa SetFit con pochi esempi annotati.")
アンチパターン: 精度を唯一の指標として使用する
不均衡なデータセット (例: 95% が陰性、5% が陽性) の場合、予測するモデル 常に「負」の場合は 95% の精度が得られますが、役に立ちません。 バイナリおよびマルチクラス分類には、常に F1、精度、および再現率を使用します。 マルチラベルでは、ハミング損失とマイクロ/マクロ F1 を使用します。 データセット内のクラスの分布を決して無視しないでください。
アプローチを選択するためのガイド
| シナリオ | 推奨されるアプローチ | セットアップ時間 |
|---|---|---|
| 固定カテゴリ、大量のデータ (>5K) | 標準 BERT 微調整 | 営業時間 |
| カテゴリは固定、データは少ない (<100) | SetFit (数ショット) | Minuti |
| 変数または新しいカテゴリ | ゼロショット NLI + カスタム テンプレート | インスタント |
| マルチラベルのバランスの取れたデータセット | BERT + BCEWithLogitsLoss + しきい値調整 | 営業時間 |
| マルチラベルの不均衡なデータセット | 焦点損失 + ラベルごとのしきい値調整 | 営業時間 |
| カテゴリ階層 | トップダウンの階層分類子 | 日数 |
| ラピッドプロトタイピング | ゼロショットパイプライン | Secondi |
10. モデルのベンチマークと選択ガイド
テキスト分類に適切なモデルの選択は、タスクの種類によって異なります。 データ量、遅延要件、利用可能なハードウェア。 ここでは、標準的なデータセットに対する主なアプローチの実用的なベンチマークを示します。
ベンチマークテキスト分類 (2024-2025)
| タスク | データセット | モデル | 精度 / F1 | 注意事項 |
|---|---|---|---|---|
| 二項感情 | SST-2 (英語) | DistilBERT の微調整 | 一致率 92.7% | BERT ベースよりも 6 倍高速 |
| 二項感情 | SST-2 (英語) | RoBERTa-ラージファインチューンド | 一致率 96.4% | 最先端のEN |
| マルチクラス(6クラス) | AGニュース | BERTベースで微調整済み | 一致率 94.8% | 標準ニュースベンチマーク |
| マルチラベル(90カタログ) | ロイター-21578 | RoBERTa + BCELoss | マイクロF1 89.2% | 90のロイターカテゴリー |
| ゼロショット | Yahoo!知恵袋 | BART-ラージ-MNLI | 適合率 70.3% | トレーニングデータがありません |
| 少数ショット (8 例) | SST-2 (英語) | セットフィット (MiniLM) | 適合率 88.1% | 注目された例は 8 つだけ |
| イタリアの感情 | センティポール2016 | dbmdz BERT の微調整 | F1 91.3% | イタリアのベストモデル |
結論と次のステップ
最新のテキスト分類は、バイナリ分類をはるかに超えています。 ゼロショット、少数ショット、およびマルチラベルは、特定のアプローチを必要とする実際のシナリオです。 この記事のツールを使用すると、数ショット用の SetFit から、数ショット用の Focal Loss まで、 ゼロショット NLI から階層分類子まで、不均衡なデータセット - 基本は理解できます 本番環境でのあらゆるテキスト分類シナリオに対処します。
モダンNLPシリーズは続く
- 前の: 固有表現の認識 — BERTによるエンティティ抽出
- 次: ハギングフェイストランスフォーマー: 完全ガイド — エコシステムと API トレーナー
- 第8条: ローカルでの LLM の微調整 — コンシューマー向け GPU 上の LoRA および QLoRA
- 第9条: 意味上の類似性 — 文の埋め込みと検索用の FAISS
- 第10条: NLPモニタリング — ドリフト検出と自動再トレーニング
- 関連シリーズ: AIエンジニアリング/RAG — RAG でのルーティングとしてのゼロショット分類
- 関連シリーズ: 高度なディープラーニング — 高度な分類アーキテクチャ







