Riconoscere le entità con spaCy in ambito medico
Lavorare con entità come luoghi, brand o persone è piuttosto facile per spaCy: in un precedente articolo abbiamo anche visto come aggiungere delle nuove label ad un modello pre-esistente.
E se avessimo bisogno di addestrare il componente da zero su un dominio come quello medico?
In questo articolo, vediamo come creare una pipeline che riconosca alcune nuove entità appartenenti al mondo della medicina, come gli agenti patogeni, condizione medica o farmaci.
Come funziona spaCy
Il riconoscimento delle entità (in inglese abbreviato in NER) funziona individuando e identificando le entità presenti in un testo non strutturato nelle categorie standard come nomi di persone, posizioni, organizzazioni, date, quantità, valute, percentuali e molto altro. SpaCy, tra le tante cose, è dotato di una serie di modelli estremamente veloci che permettono il riconoscimento delle entità e l’assegnazione delle etichette ai diversi token di un testo.
SpaCy offre un’opzione per aggiungere classi arbitrarie ai sistemi di riconoscimento delle entità e aggiornare il modello per includere anche i nuovi esempi oltre alle entità già definite all’interno del modello.
In particolare, il componente della pipeline chiamato “ner” identifica gli intervalli di token che si adattano a un insieme predeterminato di entità o categorie.
Come fare
In questo caso di esempio, andremo a utilizzare un dataset JSON composto di 20 esempi che fanno riferimento al corpus CORD-19 prodotto dall’Allen Institute for AI.
Ogni esempio contiene una frase inerente la sintomatologia e gli studi effettuati sul Covid-19 e la posizione delle entità nella frase, indicando la posizione iniziale e finale.
Queste sono le informazioni utilizzate da spaCy per prendere dei nuovi esempi per addestrare un componente nella pipeline.
"The antiviral drugs amantadine and rimantadine inhibit a viral ion channel (M2 protein), thus inhibiting replication of the influenza A virus.[86] These drugs are sometimes effective against influenza A if given early in the infection but are ineffective against influenza B viruses, which lack the M2 drug target.[160] Measured resistance to amantadine and rimantadine in American isolates of H3N2 has increased to 91% in 2005.[161] This high level of resistance may be due to the easy availability of amantadines as part of over-the-counter cold remedies in countries such as China and Russia,[162] and their use to prevent outbreaks of influenza in farmed poultry.[163][164] The CDC recommended against using M2 inhibitors during the 2005–06 influenza season due to high levels of drug resistance.[165]",
{
"entities": [
[
639,
648,
"MedicalCondition"
],
[
35,
46,
"Medicine"
],
[
712,
725,
"Medicine"
],
[
20,
30,
"Medicine"
]
]
}
La complessità in questo dominio è data dal fatto che le entità nel dominio medico possono essere rappresentate da numeri e unità di misura, caratteri alfanumerici che rappresentano principi attivi, abbreviazioni o anche parole composte.
Questo corpus include tre tipologie di entità, chiamate Medicine, MedicalCondition e Pathogen, etichettate con le label indicate in precedenza.
Dovremo eseguire diversi passi:
- caricare il dataset;
- pre-processing del dataset;
- preparare una pipeline da zero;
- addestrare il componente NER;
- testare il risultato ottenuto.
Il primo step sarà quindi quello di caricare il dataset e lo faremo tramite la libreria json:
import json
with open("corona.json") as f:
data = json.loads(f.read())
A questo punto, andiamo a preprocessare il dataset: il format che spaCy si aspetta per l’oggetto Example è il seguente:
(FRASE, {'entities': [(START, END, LABEL), ...)]})
Andiamo quindi a creare una lista che conterrà il nostro dataset di addestramento e, per ogni frase, andiamo a creare una tupla che contenga le informazioni così come nell’esempio precedente.
Per ogni oggetto presente nel dataset, ’text’ conterrà la frase e ‘annotations’ conterrà le entità; l’entità viene rappresentata dall’inizio e la fine della stringa nella frase, e dall’etichetta ad esso associata. Aggiungiamo alla lista della frase queste entità e poi aggiungiamo il risultato finale alla lista TRAIN_DATA:
TRAIN_DATA = []
for (text, annotations) in data:
new_anno = []
for annotation in annotations["entities"]:
st, end, label = annotation
new_anno.append((st, end, label))
TRAIN_DATA.append((text, {"entities": new_anno}))
print(TRAIN_DATA[0])
A questo punto, dobbiamo creare una pipeline che potremo addestrare da zero: per farlo, andremo a definire un modello vuoto tramite la funzione blank() per la lingua inglese:
nlp = spacy.blank("en")
Alla pipeline, andremo ad aggiungere il componente NER:
ner = nlp.add_pipe("ner")
Il componente dovrà avere un elenco di label da cui apprendere: andremo quindi a fornire le etichette elencate in precedenza al componente con la funzione add_label()
for ent in labels:
ner.add_label(ent)
print(ner.labels)
Per far sì che l’unico componente che andremo ad addestrare sia quello per la Named Entity Recognition, andiamo a disabilitare gli altri componenti, tutti a eccezione di quello chiamato “ner”:
other_pipes = [pipe for pipe in nlp.pipe_names if pipe != 'ner']
L’ultimo step prima di testare la pipeline è quella di creare un oggetto optimizer che aiuterà il nostro modello durante l’addestramento; in questo caso, useremo la funzione begin_training() per far sì che il modello dimentichi tutto quello che ha appreso in precedenza e inizi con dei pesi associati alla rete neurale pari a 0.
with nlp.disable_pipes(*other_pipes):
optimizer = nlp.begin_training()
Definiamo 100 epoche e poi, per ogni epoca, mischiamo in maniera random gli esempi del training set e addestriamo il componente NER con questi dati:
for i in range(0, epochs):
random.shuffle(TRAIN_DATA)
print("Epoch:", i)
for text, annotation in TRAIN_DATA:
doc = nlp.make_doc(text)
example = Example.from_dict(doc, annotation)
nlp.update([example], sgd=optimizer)
Il numero di epoche è abbastanza basso, considerata la dimensione degli esempi. Sarebbe utile aumentare a 500, o anche di più se il dataset venisse allargato!
L’addestramento potrebbe durare all’incirca 15 minuti.
Test
È arrivato il momento di testare il modello. Usiamo un esempio preso da una pubblicazione online sullo streptococco che contiene diversi termini che si riferiscono ad agenti patogeni e sintomi, e testiamo il modello:
doc = nlp("Acute Streptococcus pyogenes infections may take the form of pharyngitis, scarlet fever (rash), impetigo, cellulitis, or erysipelas. Invasive infections can result in necrotizing fasciitis, myositis and streptococcal toxic shock syndrome. Patients may also develop immune-mediated sequelae such as acute rheumatic fever and acute glomerulonephritis. S agalactiae may cause meningitis, neonatal sepsis, and pneumonia in neonates; adults may experience vaginitis, puerperal fever, urinary tract infection, skin infection, and endocarditis. Viridans streptococci can cause endocarditis, and Enterococcus is associated with urinary tract and biliary tract infections. Anaerobic streptococci participate in mixed infections of the abdomen, pelvis, brain, and lungs.")
print("### ENTS")
print(doc.ents)
Tip
Per visualizzare le entità associate al nostro caso di esempio, possiamo usare displacy: si tratta di una libreria di spaCy che ci permette di visualizzare su un browser le frasi con le etichette associate.
Sarà sufficiente importare la libreria e poi usare la funzione serve() passando come parametri il documento che contiene le nostre entità e il tipo di renderizzazione che deve utilizzare, ossia le entità:
from spacy import displacy
displacy.serve(doc, style="ent")
Risorse utili
_