고급 텍스트 분류: 다중 라벨, 제로샷 및 퓨샷
La 텍스트 분류 가장 일반적인 NLP 작업 중 하나이지만 실제로는 그것은 단순히 “긍정적이거나 부정적인” 것 이상의 의미를 갖습니다. 뉴스 기사도 마찬가지일 수 있다 정치적, 경제적, 국제적 시간. 지원 티켓은 여러 티켓에 속할 수 있습니다. 카테고리를 동시에 제공합니다. 문서는 본 적도 없이 분류될 수 있습니다. 훈련 중 해당 카테고리의 예.
이 기사에서는 복잡한 텍스트 분류를 다룹니다. 다중 클래스(상호 배타적인 여러 클래스), 다중 레이블(여러 동시 레이블), 계층적 분류 및 제로샷 분류(훈련 예제 없음) 대상 클래스의 경우). 이탈리아어 데이터 세트인 Focal Loss를 사용한 구현을 포함합니다. 불균형 데이터세트 및 완전한 프로덕션 파이프라인을 위한 것입니다.
이 시리즈의 여섯 번째 기사입니다 최신 NLP: BERT에서 LLM까지. BERT 및 HuggingFace 생태계에 대해 잘 알고 있다고 가정합니다.
무엇을 배울 것인가
- 이진 분류, 다중 클래스 및 다중 레이블 분류의 차이점 - 언제 무엇을 사용할지
- 소프트맥스 및 복합 측정항목을 사용한 다중 클래스 분류를 위한 BERT 미세 조정
- 시그모이드, BCEWithLogitsLoss 및 Focal Loss를 사용한 다중 레이블 분류
- F1 최적화를 통한 다중 라벨의 라벨별 임계값 조정
- NLI 모델(BART, DeBERTa-v3) 및 사용자 정의 템플릿을 사용한 제로샷 분류
- SetFit을 사용한 퓨샷 분류: 클래스당 8-64개 예시
- 평면 대 하향식의 계층적 분류
- 불균형 데이터 세트 관리: 클래스 가중치, 초점 손실, 오버샘플링
- 다중 레이블 측정항목: 해밍 손실, 마이크로/매크로 F1, 하위 집합 정확도
- 캐싱 및 일괄 추론을 갖춘 프로덕션 준비 분류 파이프라인
1. 텍스트 분류 분류
올바른 분류 유형을 선택하는 것이 중요한 첫 번째 단계입니다. 선택은 손실 함수, 평가 지표 및 모델 아키텍처에 영향을 미칩니다.
텍스트 분류 유형: 선택 안내
| 유형 | 설명 | 실제 사례 | 출력 레이어 | 손실 함수 | 주요 지표 |
|---|---|---|---|---|---|
| 바이너리 | 2개의 상호 배타적인 클래스 | 스팸 대 햄, 긍정 대 부정 | 시그모이드(1) | BCEWithLogits손실 | F1, AUC-ROC |
| 다중 클래스 | N개 클래스, 선택 1 | 뉴스 카테고리, 텍스트 언어 | 소프트맥스(N) | 교차엔트로피손실 | 정확도, F1 매크로 |
| 다중 라벨 | N개 클래스, 동시에 여러 활성 | 기사 태그, 다양한 감정 | 시그모이드(N) | BCEWithLogits손실 | 해밍 손실, 마이크로 F1 |
| 계층적 | 계층 구조로 구성된 클래스 | 제품 카테고리(전자제품 > TV > OLED) | 의존한다 | 계층적 손실 | 레벨별 F1 |
| 제로샷 | 훈련 중 본 적 없는 수업 | 임의의 주제에 대한 라우팅 | NLI 수반 점수 | NLI 훈련 손실 | F1, 클래스별 정확도 |
| 퓨샷(SetFit) | 수업당 몇 가지 예(8-64) | 도메인별 분류 | 물류 책임자 | 대비 + CE | 정확도, F1 |
2. BERT를 이용한 다중 클래스 분류
다중 클래스의 경우 각 예에 대해 하나의 클래스만 정확합니다. 사용하자 소프트맥스 출력 레이어의 활성화 함수로 전자 교차엔트로피손실 손실 함수로. BERT는 AG News(~95%), yelp-full(~70%), 야후 답변(~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개 이상의 활성 라벨이 있을 수 있습니다. 멀티클래스에 비해 근본적인 변화는 출력 레이어에 있습니다. (softmax 대신 sigmoid) 및 손실 함수(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"로 분류될 수 있습니다. 두 가지 주요 접근 방식이 있습니다. 플랫 (계층 무시) 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 + 임계값 조정 | 시간 |
| 다중 라벨, 불균형 데이터 세트 | 초점 손실 + 라벨별 임계값 조정 | 시간 |
| 카테고리 계층 | 하향식 계층 분류자 | Giorni |
| 신속한 프로토타이핑 | 제로샷 파이프라인 | Secondi |
10. 모델 벤치마크 및 선택 가이드
텍스트 분류에 적합한 모델을 선택하는 것은 작업 유형에 따라 다릅니다. 데이터 볼륨, 대기 시간 요구 사항 및 사용 가능한 하드웨어. 다음은 표준 데이터 세트에 대한 주요 접근 방식의 실제 벤치마크입니다.
벤치마크 텍스트 분류(2024-2025)
| 작업 | 데이터세트 | 모델 | 정확도 / F1 | 메모 |
|---|---|---|---|---|
| 바이너리 감정 | SST-2 (EN) | DistilBERT 미세 조정 | Acc 92.7% | BERT 기반보다 6배 빠릅니다. |
| 바이너리 감정 | SST-2 (EN) | RoBERTa-대형 미세 조정 | Acc 96.4% | 최신 기술 EN |
| 멀티클래스(6개 클래스) | AG 뉴스 | BERT 기반 미세 조정 | Acc 94.8% | 표준 뉴스 벤치마크 |
| 다중 라벨(90개 카테고리) | 로이터-21578 | RoBERTa + BC손실 | 마이크로-F1 89.2% | 90개의 로이터 카테고리 |
| 제로샷 | 야후 답변 | BART-대형-MNLI | Acc 70.3% | 훈련 데이터 없음 |
| 퓨샷(8개 예) | SST-2 (EN) | SetFit (MiniLM) | Acc 88.1% | 언급된 사례는 8개뿐 |
| 이탈리아 감성 | 센티폴크 2016 | dbmdz BERT 미세 조정 | F1 91.3% | 최고의 이탈리아 모델 |
결론 및 다음 단계
최신 텍스트 분류는 이진 분류를 훨씬 뛰어넘습니다. 제로샷, 퓨샷 및 다중 라벨은 특정 접근 방식이 필요한 실제 시나리오입니다. 이 기사의 도구를 사용하여 몇 장의 사진을 위한 SetFit부터 몇 장의 사진을 위한 초점 손실까지 제로샷 NLI부터 계층적 분류자까지 불균형 데이터 세트 — 기본 사항을 갖추고 있습니다. 프로덕션의 모든 텍스트 분류 시나리오를 해결합니다.
현대 NLP 시리즈는 계속됩니다
- 이전의: 명명된 엔터티 인식 — BERT를 사용한 엔터티 추출
- 다음: HuggingFace Transformers: 전체 가이드 — 생태계 및 API 트레이너
- 제8조: 로컬에서 LLM 미세 조정 — 소비자 GPU의 LoRA 및 QLoRA
- 제9조: 의미론적 유사성 — 검색을 위한 문장 임베딩 및 FAISS
- 제10조: NLP 모니터링 — 드리프트 감지 및 자동 재교육
- 관련 시리즈: AI 엔지니어링/RAG — RAG의 라우팅으로 제로샷 분류
- 관련 시리즈: 고급 딥러닝 — 고급 분류 아키텍처







