Riconoscimento delle entità nominate

Grazie ai formati di dati incorporati come il microformato h-recipe e lo schema di ricette di Schema.org, molte delle ricette pubblicate sul Web sono contrassegnate semanticamente. Ancora meglio, c’è una gemma di Rubino chiamata hangry per analizzare questi formati. In breve tempo, ho trasformato le ricette in dati strutturati.
La cosa che mi interessava di più erano gli ingredienti, e qui ho trovato il mio prossimo problema: avevo liste di ingredienti leggibili dall’uomo, niente di abbastanza strutturato per confrontare quantità, trovare somiglianze o convertire unità.
Gli ingredienti sono difficili
I primi esempi che ho guardato mi sono sembrati piuttosto semplici:
[
“2 cucchiai di burro”.
“2 cucchiai di farina”.
“1/2 tazza di vino bianco”.
“1 tazza di brodo di pollo.”
]
Sembrava che stesse emergendo uno schema chiaro, e forse sarebbe bastata una linea di codice Ruby:
quantità, unità, nome = description.split(” “, 3)
Purtroppo la realtà era molto più complessa. Ho trovato sempre più esempi che non si adattavano a questo semplice schema. Alcuni ingredienti avevano quantità multiple che dovevano essere combinate (“3 tazze e 2 cucchiai”, o “2 confezioni da 10 once”); alcuni avevano quantità alternative in metrico e imperiale, sia in tazze che in once; altri seguivano il nome dell’ingrediente con le istruzioni di preparazione, o elencavano diversi ingredienti insieme nello stesso articolo.
I casi speciali si accumulavano sempre più alti, e il mio semplice codice Ruby diventava sempre più aggrovigliato. Ho smesso di sentirmi a mio agio con il codice, poi ho smesso di pensare che avrebbe funzionato dopo la rifattorizzazione, e alla fine l’ho buttato via.
Avevo bisogno di un piano completamente nuovo.
Riconoscimento delle entità nominate
Questo mi è sembrato il problema perfetto per l’apprendimento macchina supervisionato – avevo molti dati che volevo categorizzare; categorizzare un singolo esempio manualmente era abbastanza facile; ma identificare manualmente un modello ampio era nel migliore dei casi difficile, e nel peggiore dei casi impossibile.
Quando ho considerato le mie opzioni, il riconoscimento di un’entità nominata mi è sembrato lo strumento giusto da utilizzare. I riconoscimenti di entità nominate identificano categorie predefinite nel testo; volevo che nel mio caso ne riconoscesse i nomi, le quantità e le unità di ingredienti.
Ho deciso per il NER di Stanford, che utilizza un modello di sequenza di campi condizionali casuali. Ad essere onesti, non capisco la matematica che sta dietro a questo particolare tipo di modello, ma è possibile leggere il documento1 se si vogliono tutti i dettagli cruenti. Per me era importante poter addestrare questo modello NER sul mio set di dati.
Il processo che ho dovuto seguire per addestrare il mio modello si basava sull’esempio di Jane Austen tratto dalle Stanford NER FAQ.
Addestrare il modello
La prima cosa che ho fatto è stata raccogliere i miei dati campione. All’interno di una singola ricetta, il modo in cui gli ingredienti sono scritti è abbastanza uniforme. Volevo essere sicuro di avere una buona gamma di formati, così ho combinato gli ingredienti di circa 30.000 ricette online in un’unica lista, le ho ordinate a caso e ho scelto le prime 1.500 per il mio set di allenamento.
Sembrava proprio così:
zucchero per spolverare la torta
1 tazza e 1/2 tazza di salmone affumicato a dadini
1/2 tazza di mandorle intere (3 oz), tostate
…
Poi, ho usato parte della suite di strumenti GNP di Stanford per dividerli in chip.
Il seguente comando leggerà il testo da input standard, e i token di output in output standard:
java -cp stanford-ner.jar edu.stanford.nlp.process.PTBTokenizer
In questo caso, ho voluto costruire un modello che includesse la descrizione di un singolo ingrediente, non una serie completa di descrizioni degli ingredienti. Nel linguaggio del PNL, ciò significa che ogni descrizione degli ingredienti deve essere considerata un documento separato. Per rappresentare questo agli strumenti NER di Stanford, dobbiamo separare ogni set di gettoni con una riga vuota.
Li ho rotti usando un piccolo script a conchiglia:
durante la lettura della riga; fare
echo $line | java -cp stanford-ner.jar edu.stanford.nlp.process.PTBTokenizer >> train.tok
eco >> treno.tok
fatto < treno.txt
Diverse ore dopo, i risultati sono stati simili a questo:
NOME pasticceri
NOME
NOME DELLO ZUCCHERO
per O
spolvero O
la O
torta O
1 1/2 QUANTITÀ
tazze UNITÀ
tagliato a dadini O
NOME FUMO
salmone NOME
1/2 QUANTITÀ
Tazza UNITÀ
intera O
mandorle NOME
-LRB- O
3 QUANTITÀ
OZ UNITÀ
-RRB- O
, O
O tostato
…
Ora che il set di formazione era finito, ho potuto costruire il modello:
java -cp stanford-ner.jar edu.stanford.nlp.ie.crf.CRFClassifier \\x22
-treinFile train.tsv \\x22
-serializeTo ner-model.ser.gz \\x22
-treno.prop
Il file train.prop che ho usato era molto simile al file di esempio di Stanford NER FAQ, austen.prop.
Test del modello
Uno degli aspetti negativi dell’apprendimento automatico è che è un po’ opaco. Sapevo di aver addestrato un modello, ma non sapevo quanto sarebbe stato preciso. Fortunatamente, Stanford fornisce strumenti di prova per farvi sapere quanto bene il vostro modello può generalizzare a nuovi esempi.
Ho preso circa altri 500 esempi a caso dal mio set di dati, e sono passato attraverso lo stesso affascinante processo di etichettatura manuale dei gettoni. Ora avevo una serie di test che potevo utilizzare per convalidare il mio modello. Le nostre misurazioni di precisione si baseranno su come le etichette dei gettoni prodotte dal modello differiscono dalle etichette dei gettoni che ho scritto a mano.
Ho testato il modello usando questo comando:
Ho testato il modello usando questo comando:
java -cp stanford-ner.jar edu.stanford.nlp.ie.crf.CRFClassifier ¿
-loadClassificatore ner-modello.ser.gz \\\x22
-testFile text.tsv
Questo comando di prova emette i dati di prova con l’etichetta che avevo dato ad ogni token e l’etichetta il modello previsto per ogni token, seguito da un riassunto della precisione:
Il CRFClassifier ha etichettato 4539 parole in 514 documenti a 3953,83 parole al secondo.
Entità P R F1 TP FP FN
NOME 0,8327 0,7764 0,8036 448 90 129
QUANTITÀ 0,9678 0,9821 0,9749 602 20 11
UNITÀ 0,9501 0,9630 0,9565 495 26 19
Totale 0,9191 0,9067 0,9129 1545 136 159
Le intestazioni delle colonne sono un po’ opache, ma sono metriche standard di machine learning che hanno un buon senso con una piccola spiegazione.
P è precisione: è il numero di gettoni di un dato tipo che il modello ha identificato correttamente, sul numero totale di gettoni che il modello previsto era di quel tipo. L’83% dei token che il modello identificato come NAME token erano in realtà NAME token, il 97% dei token che il modello identificato come QUANTITY token erano in realtà QUANTITY token, ecc.
R è recall: è il numero di gettoni di un dato tipo che il modello ha identificato correttamente, rispetto al numero totale di gettoni di quel tipo nel set di prova. Il modello ha trovato il 78% dei token NAME, il 98% dei token QUANTITY, ecc.
F è il punteggio F1, che combina precisione e richiamo. È possibile che un modello sia molto impreciso, ma che abbia comunque un punteggio elevato in termini di precisione o di ricalcolo: se un modello avesse etichettato ogni gettone come NOME, otterrebbe un ottimo punteggio di richiamo. Combinando i due come punteggio F1 si ottiene un unico numero più rappresentativo della qualità complessiva.
TP, FP e FN sono rispettivamente veri positivi, falsi positivi e falsi negativi.
Utilizzando il modello
Ora avevo un modello e la fiducia che fosse ragionevolmente accurato, potevo usarlo per classificare nuovi esempi che non erano nel training o nei test set di prova.
Ecco il comando per eseguire il modello:
$ echo “1/2 tazza di farina” | | |
java -cp stanford-ner/stanford-ner.jar edu.stanford.nlp.ie.crf.CRFClassifier \\\x22
-loadClassificatore ner-modello.ser.gz \\\x22
-readStdin
Invocato il mercoledì 27 settembre 08:18:42 EDT 2017 con argomentazioni: -loadClassifier
ner-modello.ser.gz -readStdin
loadClassifier=ner-modello.ser.gz
readStdin=vero
Caricamento del classificatore da ner-model.ser.gz … fatto [0,3 sec].
1/2/QUANTITÀ tazza/UNITÀ di/O farina/NOME
CRFClassifier ha etichettato 4 parole in 1 documento a 18,87 parole al secondo.
$ echo “1/2 tazza di farina” | | |
java -cp stanford-ner/stanford-ner.jar edu.stanford.nlp.ie.crf.CRFClassifier \\\x22
-loadClassificatore ner-modello.ser.gz \\\x22
-readStdin 2>/dev/null
1/2/QUANTITÀ tazza/UNITÀ di/O farina/NOME
Iterate sul modello
Anche con questi punteggi di F1 apparentemente alti, il modello era buono solo quanto il suo set di allenamento. Quando sono tornato indietro e ho eseguito la mia serie completa di descrizioni degli ingredienti attraverso il modello ho scoperto rapidamente alcuni difetti.
Il problema più evidente era che il modello non riusciva a riconoscere le once di liquido come unità. Quando ho guardato indietro al set di allenamento e al set di prova, non c’era un solo esempio di once di liquido, fl oz, o fl oz.
Il mio campione casuale non era abbastanza grande da rappresentare veramente i dati.
Ho selezionato ulteriori esempi di allenamento e di test, avendo cura di includere varie rappresentazioni di once di liquido nei miei set di allenamento e di test. Il modello aggiornato ha ottenuto un punteggio simile sui set di test aggiornati e non ha più avuto problemi con le once fluide.