Adlandırılmış Varlık Tanıma: Metinden Bilgi Çıkarma
NLP sistemleri her gün milyarlarca veriden yapılandırılmış bilgiyi otomatik olarak çıkarıyor. Belgeler: haberler, sözleşmeler, e-postalar, tıbbi kayıtlar, sosyal medya. Motor bu ekstraksiyonun adı Adlandırılmış Varlık Tanıma (NER), bir görev Metinde adı geçen varlıkları (kişiler, kuruluşlar, yerler, tarihler, parasal tutarlar ve çok daha fazlası.
NER, birçok bilgi çıkarma hattındaki ilk adımdır: bilmeden kim neyi, nerede ve ne zaman yapıyor, bilgi grafikleri oluşturamıyoruz, power RAG systems, automate contracts or analyze financial news. Bu yazıda spaCy ile temelden NER sistemleri oluşturacağız İtalyancaya özel önem vererek BERT'in ince ayarına.
Ne Öğreneceksiniz
- NER nedir ve ana varlık kategorileri (PER, ORG, LOC, DATE, MONEY...)
- Belirteç açıklaması için BIO (Başlangıç-İç-Dış) formatı
- spaCy ile NER: önceden eğitilmiş modeller ve kişiselleştirme
- HuggingFace Transformers ile NER için BERT'e ince ayar yapma
- Metrikler: aralık düzeyinde F1, hassasiyet, sıralı geri çağırma
- Etiket hizalaması için WordPiece tokenizasyonunu yönetme
- spaCy it_core_news ve İtalyan BERT modelleri ile İtalyanca için NER
- Uzun belgelerde NER: kayan pencere ve son işleme
- Gelişmiş mimariler: NER için CRF katmanı, RoBERTa, DeBERTa
- Uçtan uca üretim hattı, görselleştirme ve örnek olay incelemesi
1. Varlık Tanıma Nedir?
NER'in görevi belirteç sınıflandırması: metindeki her belirteç için, model, adlandırılmış bir varlığın parçası olup olmadığını ve ne tür bir varlık olduğunu tahmin etmelidir. Cümle sınıflandırmasının (cümle başına bir çıktı üreten) aksine, NER her token için çıktı üretir; bu onu daha karmaşık hale getirir hem model hem de işlem sonrası bakış açısından.
NER örneği
Giriş: "Elon Musk, Tesla'yı 2003 yılında San Carlos, Kaliforniya'da kurdu."
Açıklamalı Çıktı:
- Elon Musk → BAŞINA (kişi)
- Tesla'nın → ORG (organizasyon)
- 2003 → TARİH (tarih)
- San Carlos → LOC (yer)
- Kaliforniya → LOC (yer)
1.1 ORGANİK Format
NER ek açıklaması şu formatı kullanır: BIO (Başlangıç-İç-Dış):
- B-TİPİ: TYPE türündeki bir varlığın ilk belirteci
- I-TİPİ: TYPE türündeki bir varlığın dahili belirteci
- O: jeton herhangi bir varlığa ait değil
# Esempio formato BIO
sentence = "Elon Musk fondò Tesla a San Carlos nel 2003"
bio_labels = [
("Elon", "B-PER"), # inizio persona
("Musk", "I-PER"), # interno persona
("fondò", "O"),
("Tesla", "B-ORG"), # inizio organizzazione
("a", "O"),
("San", "B-LOC"), # inizio luogo
("Carlos", "I-LOC"), # interno luogo
("nel", "O"),
("2003", "B-DATE"), # data
]
# Formato BIOES (esteso): aggiunge S-TIPO per entità di un solo token
# S-Tesla = singolo token ORG
# Il formato BIO è il più comune nei dataset NER moderni
# Label set per CoNLL-2003 (dataset NER più usato):
CONLL_LABELS = [
'O',
'B-PER', 'I-PER', # persone
'B-ORG', 'I-ORG', # organizzazioni
'B-LOC', 'I-LOC', # luoghi
'B-MISC', 'I-MISC', # miscellanea
]
1.2 NER Karşılaştırmaları ve Veri Kümeleri
Karşılaştırma için Standart NER Veri Kümesi
| Veri kümeleri | Dil | Varlık | Tren boyutu | En İyi F1 |
|---|---|---|---|---|
| CoNLL-2003 | EN | FOR, ORG, LOC, ÇEŞİTLİ | 14.041 gönderildi | ~%94 (DeBERTa) |
| OntoNotes 5.0 | EN | 18 tip | ~75K gönderildi | ~%92 |
| Evalita 2009 NER | IT | BAŞINA, ORG, LOC, GPE | ~10K gönderildi | ~%88 |
| WikiNEuRal TR | IT | FOR, ORG, LOC, ÇEŞİTLİ | ~40K gönderildi | ~%90 |
| I2B2 2014 | TR (doktor) | PHI (anonimleştirme) | 27 bin gönderildi | ~%97 |
2. spaCy ile NER
uzay İtalyanca dahil birçok dil için önceden eğitilmiş NER modelleri sunar. Ve bir üretim NER sistemi için en hızlı başlangıç noktası.
2.1 NER spaCy ile kullanıma hazır
import spacy
from spacy import displacy
# Carica modello italiano con NER
# python -m spacy download it_core_news_lg
nlp_it = spacy.load("it_core_news_lg")
# Modello inglese per confronto
# python -m spacy download en_core_web_trf
nlp_en = spacy.load("en_core_web_trf") # Transformer-based, più preciso
# NER su testo italiano
text_it = """
Il presidente Sergio Mattarella ha incontrato ieri a Roma il CEO di Fiat Stellantis
Carlos Tavares per discutere del piano industriale 2025-2030.
L'incontro e avvenuto al Quirinale e ha riguardato investimenti per 5 miliardi di euro.
"""
doc_it = nlp_it(text_it)
print("=== Entità in italiano ===")
for ent in doc_it.ents:
print(f" '{ent.text}' -> {ent.label_} ({spacy.explain(ent.label_)})")
# NER su testo inglese
text_en = "Apple CEO Tim Cook announced a new $3 billion investment in Austin, Texas on Monday."
doc_en = nlp_en(text_en)
print("\n=== Entities in English ===")
for ent in doc_en.ents:
print(f" '{ent.text}' -> {ent.label_}")
# Visualizzazione HTML (utile in Jupyter)
html = displacy.render(doc_en, style="ent", page=False)
with open("ner_visualization.html", "w") as f:
f.write(html)
2.2 İtalyanca için spaCy Varlık Kategorileri
| Etiket | Tip | Örnek |
|---|---|---|
| İÇİN | Kişi | Mario Draghi, Sophia Loren |
| ORG | Organizasyon | ENI, Juventus, İtalya Bankası |
| LOC | genel yer | Alpler, Akdeniz |
| GPE | Jeopolitik varlık | İtalya, Roma, Lombardiya |
| TARİHLER | Tarih/dönem | 3 Mart 2024 yazı |
| PARA | Para birimi | 5 milyar euro |
| ÇEŞİTLİ | Diğer | Dünya Kupası, COVID-19 |
2.3 SpaCy ile Özel Bir NER Modelinin Eğitimi
import spacy
from spacy.training import Example
import random
# Dati di training annotati (con offset carattere)
TRAIN_DATA = [
(
"La startup Satispay ha raccolto 320 milioni dalla BAFIN.",
{"entities": [(11, 19, "ORG"), (39, 53, "MONEY"), (58, 63, "ORG")]}
),
(
"Andrea Pirlo allena la Juve a Torino.",
{"entities": [(0, 12, "PER"), (23, 27, "ORG"), (30, 36, "LOC")]}
),
(
"Ferrari ha presentato la nuova SF-23 al Gran Premio di Monza.",
{"entities": [(0, 7, "ORG"), (29, 34, "MISC"), (38, 60, "MISC")]}
),
]
def train_custom_ner(train_data, n_iter=30):
"""Addestra un componente NER personalizzato su spaCy."""
nlp = spacy.blank("it")
ner = nlp.add_pipe("ner")
# Aggiungi etichette
for _, annotations in train_data:
for _, _, label in annotations.get("entities", []):
ner.add_label(label)
# Training
other_pipes = [pipe for pipe in nlp.pipe_names if pipe != "ner"]
with nlp.disable_pipes(*other_pipes):
optimizer = nlp.begin_training()
for i in range(n_iter):
random.shuffle(train_data)
losses = {}
for text, annotations in train_data:
doc = nlp.make_doc(text)
example = Example.from_dict(doc, annotations)
nlp.update([example], sgd=optimizer, losses=losses)
if (i + 1) % 10 == 0:
print(f"Iteration {i+1}: losses = {losses}")
return nlp
custom_nlp = train_custom_ner(TRAIN_DATA)
# Test
test_text = "Enel ha investito 2 miliardi a Milano."
doc = custom_nlp(test_text)
for ent in doc.ents:
print(f" '{ent.text}' -> {ent.label_}")
3. BERT ve HuggingFace Transformatörleriyle NER
Transformatör modelleri çoğu NER kıyaslamasında spaCy'den daha iyi performans gösteriyor, özellikle karmaşık metinlerde veya öğelerin belirsiz olduğu durumlarda. Ancak daha fazla veri ve eğitim süresi gerektirirler.
3.1 CoNLL-2003 Veri Kümesi
from datasets import load_dataset
# CoNLL-2003: dataset NER standard (inglese)
dataset = load_dataset("conll2003")
print(dataset)
# train: 14,041 | validation: 3,250 | test: 3,453
# Struttura del dataset
example = dataset['train'][0]
print("Tokens:", example['tokens'])
print("NER tags:", example['ner_tags'])
# Tokens: ['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']
# NER tags: [3, 0, 7, 0, 0, 0, 7, 0, 0]
# (3=B-ORG, 0=O, 7=B-MISC)
# Mappa da ID a label
label_names = dataset['train'].features['ner_tags'].feature.names
print("Labels:", label_names)
# ['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']
3.2 Belirteç-Etiket Hizalama Sorunu
BERT, WordPiece tokenizasyonunu kullanır: bir kelime birden fazla alt tokene bölünebilir. NER etiketlerini (kelime düzeyinde) BERT alt belirteçleriyle hizalamamız gerekiyor. Bu, spaCy'de olmayan, Transformers'lı NER'in spesifik zorluklarından biridir.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
# Esempio: parola "Johannesburg" e le sue label
words = ["Johannesburg", "is", "the", "largest", "city"]
word_labels = ["B-LOC", "O", "O", "O", "O"]
# Tokenizzazione WordPiece
tokenized = tokenizer(
words,
is_split_into_words=True, # input già tokenizzato a livello di parola
return_offsets_mapping=True
)
print("Subword tokens:", tokenizer.convert_ids_to_tokens(tokenized['input_ids']))
# ['[CLS]', 'Johann', '##es', '##burg', 'is', 'the', 'largest', 'city', '[SEP]']
# Allineamento etichette (strategia: -100 per subtoken non-first)
def align_labels(tokenized, word_labels, label2id):
word_ids = tokenized.word_ids()
label_ids = []
prev_word_id = None
for word_id in word_ids:
if word_id is None:
# Token speciale [CLS] o [SEP]
label_ids.append(-100)
elif word_id != prev_word_id:
# Primo subtoken della parola: usa la vera etichetta
label_ids.append(label2id[word_labels[word_id]])
else:
# Subtoken successivi: -100 (ignorati nella loss)
label_ids.append(-100)
prev_word_id = word_id
return label_ids
label2id = {"O": 0, "B-LOC": 1, "I-LOC": 2, "B-PER": 3, "I-PER": 4,
"B-ORG": 5, "I-ORG": 6, "B-MISC": 7, "I-MISC": 8}
aligned = align_labels(tokenized, word_labels, label2id)
tokens = tokenizer.convert_ids_to_tokens(tokenized['input_ids'])
for tok, lab in zip(tokens, aligned):
print(f" {tok:15s}: {lab}")
# [CLS] : -100
# Johann : 1 (B-LOC)
# ##es : -100 (ignorato)
# ##burg : -100 (ignorato)
# is : 0 (O)
# ...
3.3 NER için tam ince ayar
from transformers import (
AutoModelForTokenClassification,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForTokenClassification
)
from datasets import load_dataset
import evaluate
import numpy as np
# Configurazione
MODEL_NAME = "bert-base-cased"
DATASET_NAME = "conll2003"
MAX_LENGTH = 128
dataset = load_dataset(DATASET_NAME)
label_names = dataset['train'].features['ner_tags'].feature.names
num_labels = len(label_names)
id2label = {i: l for i, l in enumerate(label_names)}
label2id = {l: i for i, l in enumerate(label_names)}
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
def tokenize_and_align_labels(examples):
tokenized = tokenizer(
examples["tokens"],
truncation=True,
max_length=MAX_LENGTH,
is_split_into_words=True
)
all_labels = []
for i, labels in enumerate(examples["ner_tags"]):
word_ids = tokenized.word_ids(batch_index=i)
label_ids = []
prev_word_id = None
for word_id in word_ids:
if word_id is None:
label_ids.append(-100)
elif word_id != prev_word_id:
label_ids.append(labels[word_id])
else:
label_ids.append(-100)
prev_word_id = word_id
all_labels.append(label_ids)
tokenized["labels"] = all_labels
return tokenized
tokenized_datasets = dataset.map(
tokenize_and_align_labels,
batched=True,
remove_columns=dataset["train"].column_names
)
# Modello
model = AutoModelForTokenClassification.from_pretrained(
MODEL_NAME,
num_labels=num_labels,
id2label=id2label,
label2id=label2id
)
# Data collator con padding dinamico per NER
data_collator = DataCollatorForTokenClassification(tokenizer)
# Metriche: seqeval per NER span-level F1
seqeval = evaluate.load("seqeval")
def compute_metrics(p):
predictions, labels = p
predictions = np.argmax(predictions, axis=2)
true_predictions = [
[label_names[p] for (p, l) in zip(pred, label) if l != -100]
for pred, label in zip(predictions, labels)
]
true_labels = [
[label_names[l] for (p, l) in zip(pred, label) if l != -100]
for pred, label in zip(predictions, labels)
]
results = seqeval.compute(predictions=true_predictions, references=true_labels)
return {
"precision": results["overall_precision"],
"recall": results["overall_recall"],
"f1": results["overall_f1"],
"accuracy": results["overall_accuracy"],
}
# Training
args = TrainingArguments(
output_dir="./results/bert-ner-conll",
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,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="f1",
fp16=True,
report_to="none"
)
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
tokenizer=tokenizer,
data_collator=data_collator,
compute_metrics=compute_metrics
)
trainer.train()
# Expected F1 on CoNLL-2003 test: ~91-92% (BERT-base-cased)
# Con RoBERTa-large: ~93-94%
4. NER için Gelişmiş Mimariler
Klasik BERT ince ayarına ek olarak onu geliştiren mimari varyantlar da mevcuttur NER performance, in particular to capture dependencies between BIO labels.
4.1 BERT + CRF Katmanı
Il CRF (Koşullu Rastgele Alan) BERT uygulamaları üzerinden uygulanır
etiket dizilerindeki yapısal kısıtlamalar: örneğin bir jeton
I-ORG takip edemiyorum B-PER. Bu azaltır
Tamamen sinirsel mimarilerde yaygın dizi hataları.
# BERT + CRF con torchcrf o pytorch-crf
# pip install pytorch-crf
import torch
import torch.nn as nn
from transformers import BertModel, BertPreTrainedModel
from torchcrf import CRF
class BertCRFForNER(BertPreTrainedModel):
"""BERT fine-tuned con CRF layer per NER."""
def __init__(self, config, num_labels):
super().__init__(config)
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(config.hidden_size, num_labels)
self.crf = CRF(num_labels, batch_first=True)
self.init_weights()
def forward(self, input_ids, attention_mask, token_type_ids=None, labels=None):
outputs = self.bert(
input_ids,
attention_mask=attention_mask,
token_type_ids=token_type_ids
)
sequence_output = self.dropout(outputs[0])
emissions = self.classifier(sequence_output) # (batch, seq_len, num_labels)
if labels is not None:
# Training: calcola CRF loss (negativa log-likelihood)
loss = -self.crf(emissions, labels, mask=attention_mask.bool(), reduction='mean')
return {'loss': loss, 'logits': emissions}
else:
# Inference: decodifica Viterbi
predictions = self.crf.decode(emissions, mask=attention_mask.bool())
return {'predictions': predictions, 'logits': emissions}
# Vantaggi CRF:
# + Garantisce sequenze BIO valide (no I-X senza B-X prima)
# + Migliora F1 di ~0.5-1.5 punti su CoNLL
# Svantaggi:
# - Più lento in inferenza (Viterbi decoding O(n * L^2))
# - Più complesso da implementare
4.2 Daha Yeni Modeller: NER için RoBERTa ve DeBERTa
from transformers import AutoModelForTokenClassification, AutoTokenizer
from transformers import pipeline
# RoBERTa-large: ~1.5% F1 in più di BERT-base su CoNLL-2003
# Usa lo stesso codice ma cambia MODEL_NAME
# Per il massimo F1 su CoNLL inglese:
model_name = "Jean-Baptiste/roberta-large-ner-english"
ner_pipeline = pipeline(
"ner",
model=model_name,
aggregation_strategy="simple"
)
text = "Elon Musk's Tesla announced a new Gigafactory in Berlin, Germany, with €5B investment."
entities = ner_pipeline(text)
for ent in entities:
print(f" '{ent['word']}' -> {ent['entity_group']} (score={ent['score']:.3f})")
# DeBERTa-v3-large: stato dell'arte su molti benchmark NER
# (richiede più RAM - 900M parametri)
deberta_ner = pipeline(
"ner",
model="dslim/bert-large-NER",
aggregation_strategy="simple"
)
# Confronto benchmarks (CoNLL-2003 test F1):
# BERT-base-cased: ~92.0%
# RoBERTa-large: ~93.5%
# DeBERTa-v3-large: ~94.0%
# XLNet-large: ~93.0%
5. NER Çıkarımı ve İşlem Sonrası
Eğitimden sonra çıkarım, yeniden yapılandırmak için son işlemeyi gerektirir. belirteç aralığındaki varlıklar.
from transformers import pipeline
import torch
# Pipeline HuggingFace (gestisce automaticamente il post-processing)
ner_pipeline = pipeline(
"ner",
model="./results/bert-ner-conll",
tokenizer="./results/bert-ner-conll",
aggregation_strategy="simple" # raggruppa subtoken della stessa entità
)
texts = [
"Tim Cook presented Apple's new iPhone 16 in Cupertino last September.",
"The European Central Bank in Frankfurt raised rates by 25 basis points.",
"Enel Green Power signed a deal worth €2.5 billion with the Italian government.",
]
for text in texts:
entities = ner_pipeline(text)
print(f"\nText: {text}")
for ent in entities:
print(f" '{ent['word']}' -> {ent['entity_group']} "
f"(score={ent['score']:.3f}, start={ent['start']}, end={ent['end']})")
# Strategie di aggregazione disponibili:
# "none": restituisce tutti i token con la loro label
# "simple": raggruppa token consecutivi con lo stesso gruppo
# "first": usa il label del primo subtoken per ogni parola
# "average": media dei logit sui subtoken (più accurato)
# "max": usa il logit massimo sui subtoken
5.1 Uzun Belgelerde NER (512'den fazla jeton)
def ner_long_document(text, ner_pipeline, max_length=400, stride=50):
"""
NER su documenti più lunghi di 512 token usando sliding window.
max_length: massimo token per finestra
stride: overlap tra finestre consecutive (evita boundary artifacts)
"""
words = text.split()
all_entities = []
processed_positions = set()
for start_idx in range(0, len(words), max_length - stride):
end_idx = min(start_idx + max_length, len(words))
chunk = ' '.join(words[start_idx:end_idx])
entities = ner_pipeline(chunk)
# Aggiusta offset per la posizione nel testo originale
chunk_offset = len(' '.join(words[:start_idx])) + (1 if start_idx > 0 else 0)
for ent in entities:
abs_start = ent['start'] + chunk_offset
abs_end = ent['end'] + chunk_offset
# Evita duplicati dall'overlap
if abs_start not in processed_positions:
all_entities.append({
'word': ent['word'],
'entity_group': ent['entity_group'],
'score': ent['score'],
'start': abs_start,
'end': abs_end
})
processed_positions.add(abs_start)
if end_idx == len(words):
break
return sorted(all_entities, key=lambda x: x['start'])
# Nota: alternativa moderna con Longformer (supporta fino a 4096 token nativamente)
# da allenallenai/longformer-base-4096
6. İtalyanca için NER
İtalyanca, NER'i daha zorlu hale getiren morfolojik özelliklere sahiptir: cinsiyet ve sayı uyumu, klitik formlar, belirli tanımlıklı özel isimler ("Roma", "Milano"). Mevcut en iyi seçeneklere bakalım.
import spacy
from transformers import pipeline
# spaCy NER per l'italiano
nlp_it = spacy.load("it_core_news_lg")
italian_texts = [
"Il primo ministro Giorgia Meloni ha incontrato il presidente francese Macron a Parigi.",
"Fiat Chrysler Automobiles ha annunciato fusione con PSA Group per 50 miliardi.",
"L'AS Roma ha battuto la Lazio per 2-1 allo Stadio Olimpico domenica sera.",
"Il Tribunale di Milano ha condannato Mediaset a pagare 300 milioni a Vivendi.",
]
print("=== NER Italiano con spaCy it_core_news_lg ===")
for text in italian_texts:
doc = nlp_it(text)
entities = [(ent.text, ent.label_) for ent in doc.ents]
print(f"\nTesto: {text[:70]}")
print(f"Entità: {entities}")
# BERT NER per l'italiano
# Opzione 1: fine-tuned su WikiNEuRal
try:
it_ner = pipeline(
"ner",
model="osiria/bert-base-italian-uncased-ner",
aggregation_strategy="simple"
)
text = "Matteo Renzi ha fondato Italia Viva a Firenze nel 2019."
entities = it_ner(text)
print("\n=== BERT NER Italiano (osiria) ===")
for ent in entities:
print(f" '{ent['word']}' -> {ent['entity_group']} ({ent['score']:.3f})")
except Exception as e:
print(f"Modello non disponibile: {e}")
# Opzione 2: dbmdz/bert-base-italian-cased fine-tuned su Evalita
# Opzione 3: fine-tuning su WikiNEuRal IT con dbmdz/bert-base-italian-cased
# WikiNEuRal IT: ~40K frasi annotate, molto più grande di Evalita
print("\nAlternative per NER italiano:")
print(" 1. spaCy it_core_news_lg (più veloce, F1 ~85%)")
print(" 2. osiria/bert-base-italian-uncased-ner (più accurato, F1 ~88%)")
print(" 3. Fine-tuning custom su dati del tuo dominio (massima qualità)")
7. NER Değerlendirmesi ve Metrikleri
from seqeval.metrics import (
classification_report,
f1_score,
precision_score,
recall_score
)
# seqeval valuta a livello di span (entità completa)
# più appropriato dell'accuracy a livello di token
true_sequences = [
['O', 'B-PER', 'I-PER', 'O', 'B-ORG', 'O'],
['B-LOC', 'I-LOC', 'O', 'O', 'B-DATE', 'O'],
]
pred_sequences = [
['O', 'B-PER', 'I-PER', 'O', 'O', 'O'], # manca ORG
['B-LOC', 'I-LOC', 'O', 'O', 'B-DATE', 'O'], # perfetto
]
print("=== NER Evaluation (span-level) ===")
print(classification_report(true_sequences, pred_sequences))
print(f"Overall F1: {f1_score(true_sequences, pred_sequences):.4f}")
print(f"Overall Precision: {precision_score(true_sequences, pred_sequences):.4f}")
print(f"Overall Recall: {recall_score(true_sequences, pred_sequences):.4f}")
# Tipi di errori NER:
# 1. False Negative (Missed): entità non riconosciuta
# 2. False Positive (Spurious): entità inventata dove non c'e
# 3. Wrong Type: entità trovata ma tipo sbagliato (PER invece di ORG)
# 4. Wrong Boundary: entità trovata ma span parzialmente errato
# Differenza fondamentale:
# Token-level accuracy: conta token corretti / tot token
# Span-level F1 (seqeval): un'entità e corretta solo se
# TUTTI i suoi token hanno la label giusta
# -> molto più rigoroso e realistico
8. Örnek Olay İncelemesi: Finansal Haber Makalelerinde NER
Finansal makalelerden varlıkları çıkarmak için eksiksiz bir NER işlem hattı oluşturalım: şirketler, kişiler, parasal değerler ve tarihler.
from transformers import pipeline
from collections import defaultdict
import json
class FinancialNERExtractor:
"""
Estrattore NER specializzato per notizie finanziarie.
Estrae: aziende, persone chiave, valori e date.
"""
def __init__(self, model_name="dslim/bert-large-NER"):
self.ner = pipeline(
"ner",
model=model_name,
aggregation_strategy="simple"
)
self.entity_types = {
'ORG': 'companies',
'PER': 'people',
'MONEY': 'values',
'DATE': 'dates',
'LOC': 'locations',
'GPE': 'locations'
}
def extract(self, text: str) -> dict:
"""Estrae e organizza le entità per tipo."""
entities = self.ner(text)
result = defaultdict(list)
for ent in entities:
group = ent['entity_group']
mapped = self.entity_types.get(group)
if mapped and ent['score'] > 0.8:
result[mapped].append({
'text': ent['word'],
'score': round(ent['score'], 3),
'position': (ent['start'], ent['end'])
})
return dict(result)
def analyze_article(self, title: str, body: str) -> dict:
"""Analisi completa di un articolo finanziario."""
full_text = f"{title}. {body}"
# NER su testo completo
raw_entities = self.extract(full_text)
# Deduplica (stesso testo, posizioni diverse)
for etype, ents in raw_entities.items():
seen = set()
deduped = []
for e in ents:
if e['text'] not in seen:
seen.add(e['text'])
deduped.append(e)
raw_entities[etype] = deduped
return {
'title': title,
'entities': raw_entities,
'entity_count': sum(len(v) for v in raw_entities.values())
}
# Test
extractor = FinancialNERExtractor()
articles = [
{
"title": "Amazon acquires Whole Foods for $13.7 billion",
"body": "Jeff Bezos announced the acquisition in Seattle on June 16, 2017. Whole Foods CEO John Mackey will remain in his role. The deal is expected to close in the second half of 2017."
},
{
"title": "Tesla opens new Gigafactory in Germany",
"body": "Elon Musk inaugurated the Berlin factory in March 2022. The facility in Gruenheide will employ 12,000 people and produce 500,000 vehicles per year. The German government provided €1.3 billion in subsidies."
},
]
print("=== Financial NER Analysis ===\n")
for article in articles:
result = extractor.analyze_article(article['title'], article['body'])
print(f"Title: {result['title']}")
print(f"Total entities: {result['entity_count']}")
for etype, ents in result['entities'].items():
if ents:
texts = [e['text'] for e in ents]
print(f" {etype:12s}: {', '.join(texts)}")
print()
9. NER Boru Hattının Üretim İçin Optimizasyonu
Bir üretim NER sistemi doğruluğu, hızı ve hesaplama maliyetini dengelemelidir. Aşağıda sözcüksel ön filtrelemeyi ve toplu çıkarımı birleştiren optimize edilmiş bir işlem hattı bulunmaktadır. ve yüksek hacimli senaryolar için sonuçların önbelleğe alınması.
from transformers import pipeline
from functools import lru_cache
import hashlib
import json
import time
from typing import List, Dict, Optional
import numpy as np
class OptimizedNERPipeline:
"""
Pipeline NER ottimizzata per produzione:
- Caching dei risultati con LRU cache
- Batch processing adattivo
- Pre-filtro lessicale per ridurre il carico
- Monitoring della latenza e confidenza
"""
def __init__(
self,
model_name: str = "dslim/bert-large-NER",
batch_size: int = 8,
min_confidence: float = 0.75,
cache_size: int = 1024
):
self.ner = pipeline(
"ner",
model=model_name,
aggregation_strategy="simple",
batch_size=batch_size,
device=0 # -1 per CPU, 0 per prima GPU
)
self.min_confidence = min_confidence
self._cache: Dict[str, list] = {}
self._cache_size = cache_size
self._stats = {"hits": 0, "misses": 0, "total_time_ms": 0.0}
def _text_hash(self, text: str) -> str:
return hashlib.md5(text.encode()).hexdigest()
def extract(self, texts: List[str]) -> List[List[Dict]]:
"""Estrazione NER con caching e batch processing."""
results = [None] * len(texts)
uncached_indices = []
uncached_texts = []
# Controlla cache
for i, text in enumerate(texts):
key = self._text_hash(text)
if key in self._cache:
results[i] = self._cache[key]
self._stats["hits"] += 1
else:
uncached_indices.append(i)
uncached_texts.append(text)
self._stats["misses"] += 1
# Elabora i testi non in cache
if uncached_texts:
start = time.perf_counter()
raw_results = self.ner(uncached_texts)
elapsed_ms = (time.perf_counter() - start) * 1000
self._stats["total_time_ms"] += elapsed_ms
# Gestisci singolo vs batch
if len(uncached_texts) == 1:
raw_results = [raw_results]
for idx, raw in zip(uncached_indices, raw_results):
# Filtra per confidenza e pulisci
filtered = [
{
'word': e['word'].replace(' ##', '').strip(),
'entity_group': e['entity_group'],
'score': round(e['score'], 4),
'start': e['start'],
'end': e['end']
}
for e in raw
if e['score'] >= self.min_confidence
]
key = self._text_hash(texts[idx])
# Eviction semplice della cache (FIFO)
if len(self._cache) >= self._cache_size:
oldest_key = next(iter(self._cache))
del self._cache[oldest_key]
self._cache[key] = filtered
results[idx] = filtered
return results
def get_stats(self) -> Dict:
"""Restituisce statistiche sulla pipeline."""
total = self._stats["hits"] + self._stats["misses"]
return {
"cache_hit_rate": self._stats["hits"] / total if total > 0 else 0.0,
"avg_latency_ms": self._stats["total_time_ms"] / max(self._stats["misses"], 1),
"cache_size": len(self._cache),
**self._stats
}
# Utilizzo
ner_pipeline = OptimizedNERPipeline(min_confidence=0.80)
batch_texts = [
"Mario Draghi ha guidato la BCE dal 2011 al 2019.",
"Amazon ha acquisito MGM Studios per 8.45 miliardi di dollari.",
"Il MIT di Boston ha pubblicato una ricerca su GPT-4.",
"Sergio Mattarella e il Presidente della Repubblica Italiana.",
]
# Prima chiamata: elabora tutto
results1 = ner_pipeline.extract(batch_texts)
# Seconda chiamata: tutti in cache!
results2 = ner_pipeline.extract(batch_texts)
print("Statistiche pipeline NER:")
stats = ner_pipeline.get_stats()
for k, v in stats.items():
print(f" {k}: {v}")
print("\nRisultati estrazione:")
for text, entities in zip(batch_texts, results1):
print(f"\n Testo: {text[:60]}")
for ent in entities:
print(f" '{ent['word']}' -> {ent['entity_group']} ({ent['score']:.3f})")
9.1 NER Modellerinin Karşılaştırılması: Pratik Karşılaştırma
NER Karşılaştırması: Hız ve Doğruluk (2024-2025)
| Modeli | CONLL F1 | Hız (CPU) | Parametreler | Dil | Kullanım Örneği |
|---|---|---|---|---|---|
| spaCy it_core_news_sm | ~%80 | Çok hızlı (<5 ms) | 12 milyon | IT | Hızlı prototipleme |
| spaCy it_core_news_lg | ~%85 | Hızlı (10-20ms) | 560M | IT | CPU üretimi |
| dslim/bert-base-NER | ~%91 | Orta (50-100ms) | 110 milyon | EN | GPU üretimi |
| dslim/bert-large-NER | ~%92 | Yavaş (100-200ms) | 340M | EN | Yüksek hassasiyet |
| Jean-Baptiste/roberta-large-ner-english | ~%93,5 | Yavaş (150-250ms) | 355M | EN | Son teknoloji TR |
| osiria/bert-base-italyanca-kabuzsuz-ner | ~%88 | Orta (50-100ms) | 110 milyon | IT | En iyi BT modeli |
9.2 NER ile Veri Anonimleştirme
Otomatik anonimleştirme, hukuki, tıbbi ve GDPR'de kritik bir kullanım örneğidir kişisel verilerden oluşur. NER, PER, ORG, LOC ve DATE'i otomatik olarak tanımlayabilir Takma ad kullanmak veya hassas belgelerin hazırlanması için.
from transformers import pipeline
import re
class TextAnonymizer:
"""
Anonimizzatore di testo basato su NER.
Sostituisce entità sensibili con placeholder tipizzati.
GDPR-compliant: utile per dataset di training e log di sistema.
"""
REPLACEMENT_MAP = {
'PER': '<PERSONA>',
'ORG': '<ORGANIZZAZIONE>',
'LOC': '<LUOGO>',
'GPE': '<LUOGO>',
'DATE': '<DATA>',
'MONEY': '<IMPORTO>',
'MISC': '<ALTRO>',
}
def __init__(self, model_name="dslim/bert-large-NER"):
self.ner = pipeline(
"ner",
model=model_name,
aggregation_strategy="simple"
)
def anonymize(self, text: str, entity_types: list = None) -> dict:
"""
Anonimizza il testo sostituendo le entità.
entity_types: lista di tipi da anonimizzare (None = tutti)
"""
entities = self.ner(text)
# Filtra per tipo se specificato
if entity_types:
entities = [e for e in entities if e['entity_group'] in entity_types]
# Ordina per posizione decrescente per sostituire dal fondo
entities_sorted = sorted(entities, key=lambda e: e['start'], reverse=True)
anonymized = text
replacements = []
for ent in entities_sorted:
placeholder = self.REPLACEMENT_MAP.get(ent['entity_group'], '<ENTITA>')
original = text[ent['start']:ent['end']]
anonymized = anonymized[:ent['start']] + placeholder + anonymized[ent['end']:]
replacements.append({
'original': original,
'replacement': placeholder,
'type': ent['entity_group'],
'confidence': round(ent['score'], 3)
})
return {
'original': text,
'anonymized': anonymized,
'replacements': replacements,
'num_entities': len(replacements)
}
# Test
anonymizer = TextAnonymizer()
sensitive_texts = [
"Il paziente Mario Rossi, nato il 15 marzo 1978, e stato ricoverato all'Ospedale San Raffaele di Milano il 3 gennaio 2024 con diagnosi di polmonite.",
"La società Accenture Italia S.r.l., con sede in Via Paleocapa 7 a Milano, ha fatturato 500 milioni di euro nel 2023.",
"L'avvocato Giovanni Bianchi dello studio Chiomenti ha rappresentato Mediaset nel ricorso al TAR del Lazio.",
]
print("=== Anonimizzazione con NER ===\n")
for text in sensitive_texts:
result = anonymizer.anonymize(text, entity_types=['PER', 'ORG', 'LOC', 'GPE', 'DATE', 'MONEY'])
print(f"Originale: {result['original'][:100]}")
print(f"Anonimiz.: {result['anonymized'][:100]}")
print(f"Entità: {result['num_entities']} sostituite")
print()
10. Üretimde NER İçin En İyi Uygulamalar
Desen Önleme: Son İşlemleri Yoksay
NER modelleri BIO token düzeyinde tahminler üretir. Üretimde, her zaman yayılma alanlarını yeniden oluşturmanız, WordPiece alt belirteçlerini yönetmeniz ve filtrelemeniz gerekir güveni düşük varlıklar. Ham belirteçleri asla son kullanıcılara göstermeyin.
Anti-Pattern: Yalnızca Belirteç Doğruluğuyla Değerlendirin
CoNLL-2003'teki belirteç doğruluğu, vasat modeller için bile genellikle %98-99'dur.
çünkü çoğu tokenin etiketleri var O. Her zaman sırayı kullan
NER için tek ilgili ölçüm olan F1 yayılma düzeyi değerlendirmesi için.
NER Üretim Kontrol Listesi
- Yalnızca belirteç düzeyinde doğrulukla değil, sıralı değerle (span F1) değerlendirin
- Yanlış pozitifleri filtrelemek için güven eşiklerini (genellikle 0,7-0,85) ayarlayın
- Çakışan varlıkları yönetin (nadir ama mümkün)
- Çıkarılan varlıkları normalleştirin (tekilleştirme, kanonikleştirme)
- Etki alanı değişimlerini tespit etmek için varlık dağıtımını izleyin
- Tahminlerde hata ayıklamak için ekranı kullanın
- Çeşitli alanlardaki metinler üzerinde test yapın: haberler, sözleşmeler, sosyal medya farklı davranıyor
- İtalyanca için: it_core_news_lg (hızlı) veya WikiNEuRal IT'de ince ayarlı BERT (doğru) kullanın
Sonuçlar ve Sonraki Adımlar
NER, gerçek uygulamalardaki en kullanışlı NLP görevlerinden biridir: bilgi çıkarma, bilgi grafiklerinin oluşturulması, RAG sistemlerinin beslenmesi, verilerin anonimleştirilmesi. Basit vakalar için spaCy ve yüksek hassasiyet için ince ayarlı BERT ile tüm araçlara sahipsiniz hem İtalyanca hem de İngilizce için sağlam NER boru hatları oluşturmak.
Belirli bir alanda mükemmel sonuçların anahtarı her zaman veriler üzerinde ince ayar yapmaktır bağlamınıza açıklama ekleyin: alana özgü birkaç yüz örnek bile genel modele kıyasla performansı önemli ölçüde artırabilir.
Seri devam ediyor
- Sonraki: Çok Etiketli Metin Sınıflandırması — aynı anda birden fazla etikete sahip metinleri sınıflandırın
- Madde 7: HuggingFace Transformers: Tam Kılavuz — API Eğitmeni, Model Merkezi, optimizasyon
- Madde 8: LoRA İnce Ayarı — LLM'yi tüketici GPU'larıyla yerel olarak eğitin
- Madde 9: Anlamsal Benzerlik — RAG boru hattında bir ekstraksiyon adımı olarak NER
- İlgili seri: Yapay Zeka Mühendisliği/RAG — RAG boru hattında bir ekstraksiyon adımı olarak NER







