固有表現の認識: テキストからの情報の抽出
NLP システムは毎日、何十億ものデータから構造化情報を自動的に抽出します。 文書の種類: ニュース、契約書、電子メール、医療記録、ソーシャルメディア。エンジン この抽出の名前は 固有表現認識 (NER)、タスク テキスト内で名前が挙げられているエンティティ (人、組織、 場所、日付、金額など。
NER は、多くの情報抽出パイプラインの最初のステップです。 誰が、いつ、どこで何をするか、ナレッジグラフを構築することはできません。 RAG システムに電力を供給したり、契約を自動化したり、金融ニュースを分析したりできます。 この記事では、spaCy を使用してベースラインから NER システムを構築します。 イタリア語に特に注意を払い、BERT を微調整します。
何を学ぶか
- NER と主要なエンティティ カテゴリ (PER、ORG、LOC、DATE、MONEY...) とは何ですか?
- トークンアノテーションの BIO (Beginning-Inside-Outside) 形式
- spaCy を使用した NER: 事前トレーニングされたモデルとカスタマイズ
- HuggingFace Transformers を使用した NER 向けの BERT の微調整
- メトリック: スパンレベル F1、精度、seqeval による再現率
- ラベル配置のための WordPiece トークン化の処理
- spaCy it_core_news およびイタリア語 BERT モデルを使用したイタリア語の NER
- 長い文書の NER: スライディング ウィンドウと後処理
- 高度なアーキテクチャ: CRF レイヤー、RoBERTa、NER 用の DeBERTa
- エンドツーエンドの制作パイプライン、視覚化、ケーススタディ
1. 固有表現認識とは
NER の任務は、 トークンの分類: テキスト内の各トークンについて、 モデルは、それが名前付きエンティティの一部であるかどうか、またそのタイプを予測する必要があります。 文の分類 (文ごとに 1 つの出力が生成される) とは異なり、 NER はトークンごとに出力を生成します。これにより、より複雑になります。 モデルと後処理の両方の観点から。
NERの例
入力: 「イーロン・マスクは 2003 年にカリフォルニア州サンカルロスでテスラを設立しました。」
注釈付き出力:
- イーロン・マスク →PER(人)
- テスラ → ORG(組織)
- 2003年 →DATE(日付)
- サンカルロス →LOC(場所)
- カリフォルニア →LOC(場所)
1.1 オーガニックフォーマット
NER アノテーションは次の形式を使用します。 BIO (始まり-内部-外部):
- Bタイプ: タイプ TYPE のエンティティの最初のトークン
- I-TYPE: タイプ TYPE のエンティティの内部トークン
- O: トークンはどのエンティティにも属していません
# 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 ベンチマークとデータセット
ベンチマーク用の標準 NER データセット
| データセット | Lingua | 実在物 | 列車のサイズ | ベストF1 |
|---|---|---|---|---|
| CoNLL-2003 | EN | FOR、ORG、LOC、その他 | 14,041 件が送信されました | ~94% (デベルタ) |
| OntoNotes 5.0 | EN | 18種類 | ~75K が送信されました | ~92% |
| エヴァリタ 2009 NER | IT | PER、組織、LOC、GPE | ~10K が送信されました | ~88% |
| ウィキニューラル JP | IT | FOR、ORG、LOC、その他 | ~40K が送信されました | ~90% |
| I2B2 2014 | EN(医師) | PHI(匿名化) | 27Kを送信しました | ~97% |
2. スペイシーを備えたNER
スペイシー イタリア語を含む多くの言語に対応する事前トレーニング済み NER モデルを提供します。 実稼働 NER システムの最速の開始点です。
2.1 SPCY を備えたすぐに使える NER
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 イタリア語の spaCy エンティティ カテゴリ
| ラベル | タイプ | Esempio |
|---|---|---|
| のために | Persona | マリオ・ドラギ、ソフィア・ローレン |
| 組織 | 組織 | ENI、ユベントス、イタリア銀行 |
| LOC | 一般的な場所 | アルプス、地中海 |
| GPE | 地政学的実体 | イタリア、ローマ、ロンバルディア州 |
| 日付 | 日付・期間 | 2024年夏 3月3日 |
| お金 | 通貨 | 50億ユーロ |
| その他 | 他の | ワールドカップ、新型コロナウイルス感染症 |
2.3 spaCy を使用したカスタム NER モデルのトレーニング
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 と HuggingFace トランスフォーマーを備えた NER
Transformer モデルは、ほとんどの NER ベンチマークで spaCy を上回ります。 特に複雑なテキストの場合、またはエンティティがあいまいな場合に顕著です。 ただし、より多くのデータとトレーニング時間が必要です。
3.1 CoNLL-2003 データセット
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 トークンとラベルの位置合わせの問題
BERT は WordPiece トークン化を使用します。単語は複数のサブトークンに分割できます。 NER ラベル (単語レベル) を BERT サブトークンと揃える必要があります。 これは、spaCy には存在しない、トランスフォーマーを使用した NER 特有の課題の 1 つです。
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 の完全な微調整
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のための高度なアーキテクチャ
従来の BERT の微調整に加えて、それを改善するアーキテクチャのバリアントもあります NER のパフォーマンス、特に BIO ラベル間の依存関係をキャプチャします。
4.1 BERT + CRF レイヤ
Il CRF (条件付きランダムフィールド) BERT を介して適用されます。
ラベルシーケンスの構造的制約: トークンなど
I-ORG フォローできません B-PER。これにより、
純粋なニューラル アーキテクチャでよくあるシーケンス エラー。
# 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 最新モデル: NER 用 RoBERTa および 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 推論と後処理
トレーニング後、推論を再構築するための後処理が必要になります。 トークンスパンからのエンティティ。
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 長い文書の NER (512 トークン以上)
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. イタリア語の「NER」
イタリア語には、NER をさらに困難にする形態学的特徴があります。 性別と数の一致、接語形、定冠詞付きの固有名詞 (「ローマ」、「ミラノ」)。利用可能な最良のオプションを見てみましょう。
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の評価と指標
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. ケーススタディ: 金融ニュース記事に関する NER
金融記事からエンティティを抽出するための完全な NER パイプラインを構築してみましょう。 企業、人物、金銭的価値、日付。
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 パイプラインの最適化
本番の NER システムは、精度、速度、計算コストのバランスを取る必要があります。 以下は、字句事前フィルタリングとバッチ推論を組み合わせた最適化されたパイプラインです。 大容量シナリオの結果キャッシュ。
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 モデルの比較: 実用的なベンチマーク
NER ベンチマーク: 速度 vs 精度 (2024-2025)
| モデル | CoNLL F1 | 速度(CPU) | パラメータ | Lingua | 使用事例 |
|---|---|---|---|---|---|
| spaCy it_core_news_sm | ~80% | 非常に高速 (<5ms) | 12M | IT | ラピッドプロトタイピング |
| spaCy it_core_news_lg | ~85% | 高速 (10 ~ 20 ミリ秒) | 560M | IT | CPUの生産 |
| dslim/bert-base-NER | ~91% | 中 (50-100ms) | 110M | EN | GPUの生産 |
| dslim/bert-large-NER | ~92% | 遅い (100 ~ 200 ミリ秒) | 340M | EN | 高精度 |
| ジャン・バティスト/ロベルタ・ラージ・ナー・英語 | ~93.5% | 遅い (150-250ms) | 355M | EN | 最先端のEN |
| osiria/bert-base-italian-uncased-ner | ~88% | 中 (50-100ms) | 110M | IT | ベストITモデル |
9.2 NERによるデータ匿名化
法律、医療、GDPR における重要なユースケースは、自動匿名化です。 個人データの。 NER は PER、ORG、LOC、DATE を自動的に識別できます 仮名化または機密文書の起草のため。
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. 本番環境における NER のベスト プラクティス
アンチパターン: 後処理を無視する
NER モデルは、BIO トークン レベルで予測を生成します。生産では、 常にスパンを再構築し、WordPiece サブトークンを管理し、フィルターする必要があります 信頼性の低いエンティティ。生のトークンをエンドユーザーに決して公開しないでください。
アンチパターン: トークンの精度のみで評価する
CoNLL-2003 のトークン精度は、平凡なモデルであっても通常 98 ~ 99% です。
ほとんどのトークンにはラベルがあるため O。常に seqeval を使用する
NER に関連する唯一の指標である F1 スパンレベル評価の場合。
NER 生産チェックリスト
- トークンレベルの精度だけでなく、seqeval (スパン F1) で評価します
- 信頼度のしきい値 (通常は 0.7 ~ 0.85) を設定して、誤検知をフィルタリングします。
- 重複するエンティティの処理 (まれではありますが、可能性があります)
- 抽出されたエンティティを正規化します (重複排除、正規化)。
- エンティティの分散を監視してドメインの変化を検出する
- 予測のデバッグに表示を使用する
- さまざまなドメインのテキストをテストします: ニュース、契約書、ソーシャル メディアは異なる動作をします
- イタリア語の場合: it_core_news_lg (高速) または WikiNEuRal IT で微調整された BERT (正確) を使用します。
結論と次のステップ
NER は、実際のアプリケーションで最も役立つ NLP タスクの 1 つです。情報抽出、 ナレッジ グラフの構築、RAG システムのフィード、データの匿名化。 単純なケースには spaCy を、高精度には微調整された BERT を使用すると、すべてのツールが手に入ります。 イタリア語と英語の両方に対応する堅牢な NER パイプラインを構築します。
特定の領域で優れた結果を得る鍵は、常にデータを微調整することです コンテキストに注釈を付ける: 数百のドメイン固有の例でも 汎用モデルと比較してパフォーマンスを大幅に向上させることができます。
シリーズは続く
- 次: マルチラベルテキスト分類 — 複数のラベルを使用してテキストを同時に分類します
- 第7条: ハギングフェイストランスフォーマー: 完全ガイド — API トレーナー、モデル ハブ、最適化
- 第8条: LoRA の微調整 — コンシューマ GPU を使用して LLM をローカルでトレーニングする
- 第9条: 意味上の類似性 — RAG パイプラインの抽出ステップとしての NER
- 関連シリーズ: AIエンジニアリング/RAG — RAG パイプラインの抽出ステップとしての NER







