Dank der eingebauten Datenformate wie dem h-Rezept-Mikroformat und dem Rezeptschema von Schema.org sind viele der im Web veröffentlichten Rezepte semantisch gekennzeichnet. Noch besser: Es gibt einen Rubin-Kleinod namens hangry, um diese Formate zu analysieren. In kurzer Zeit habe ich die Rezepte in strukturierte Daten umgewandelt.

Das, was mich am meisten interessierte, waren die Zutaten, und hier fand ich mein nächstes Problem: Ich hatte Listen mit menschenlesbaren Zutaten, nichts, das strukturiert genug war, um Mengen zu vergleichen, Ähnlichkeiten zu finden oder Einheiten umzurechnen.

Ingredienzen sind schwierig

Die ersten Beispiele, die ich mir angeschaut habe, erschienen mir ziemlich einfach:

[

“2 Esslöffel Butter.”

“2 Esslöffel Mehl.”

“1/2 Tasse Weißwein.”

“1 Tasse Hühnerbrühe.”

]

Es schien sich ein klares Muster abzuzeichnen, und vielleicht hätte eine Zeile Ruby-Code ausgereicht:

Menge, Einheit, Name = Beschreibung.split(” “, 3)

Bedauerlicherweise war die Realität viel komplexer. Ich fand immer mehr Beispiele, die nicht in dieses einfache Muster passten. Einige wenige Zutaten hatten mehrere Mengen, die kombiniert werden mussten (“3 Tassen und 2 Löffel” oder “2 Packungen à 10 Unzen”); einige hatten alternative Mengen in metrischen und imperialen Maßeinheiten, entweder in Tassen und Unzen; andere folgten dem Namen der Zutat mit der Zubereitungsanleitung oder listeten mehrere Zutaten zusammen in einem Artikel auf.

Die Sonderfälle häuften sich immer mehr an, und mein einfacher Ruby-Code verwickelte sich immer mehr. Ich fühlte mich mit dem Code nicht mehr wohl, dann hatte ich nach dem Refactoring nicht mehr das Gefühl, dass er funktionieren würde, und warf ihn schließlich weg.

Ich brauchte einen ganz neuen Plan.

Erkennung von benannten Entitäten

Das schien mir das perfekte Problem für überwachtes maschinelles Lernen zu sein – ich hatte eine Menge Daten, die ich kategorisieren wollte; ein einzelnes Beispiel manuell zu kategorisieren war einfach genug; aber ein breites Muster manuell zu identifizieren war bestenfalls schwierig, schlimmstenfalls unmöglich.

Als ich meine Optionen in Betracht zog, schien mir eine Erkennung benannter Entitäten das richtige Werkzeug zu sein. Named Entity Recognizer identifizieren vordefinierte Kategorien im Text; ich wollte, dass es in meinem Fall ihre Namen, Mengen und Einheiten von Inhaltsstoffen erkennt.

Ich entschied mich für die Stanford NER, die ein Sequenzmodell von zufälligen bedingten Feldern verwendet. Um ehrlich zu sein, verstehe ich die Mathematik hinter dieser speziellen Art von Modell nicht, aber Sie können das Dokument1 lesen, wenn Sie all die blutigen Details wollen. Was mir wichtig war, war, dass ich dieses NER-Modell auf meinem Datensatz trainieren konnte.

Der Prozess, dem ich folgen musste, um mein Modell zu trainieren, basierte auf dem Beispiel von Jane Austen aus der Stanford NER FAQ.

Das Modell trainieren

Als erstes habe ich meine Beispieldaten gesammelt. Innerhalb eines einzigen Rezeptes ist die Art und Weise, wie die Zutaten geschrieben sind, ziemlich einheitlich. Ich wollte sichergehen, dass ich eine gute Auswahl an Formaten habe, also fasste ich die Zutaten aus etwa 30.000 Rezepten online in einer einzigen Liste zusammen, bestellte sie nach dem Zufallsprinzip und wählte die ersten 1.500 für mein Trainingsset aus.

Es sah genau so aus:

Zucker zum Bestäuben des Kuchens

1 Tasse und 1/2 Tasse Räucherlachs in Würfel geschnitten

1/2 Tasse ganze Mandeln (3 oz), geröstet

Dann benutzte ich einen Teil der GNP-Werkzeuge aus Stanfords Suite, um sie in Chips zu zerlegen.

Der folgende Befehl liest den Text von der Standardeingabe und die Ausgabe-Token in die Standardausgabe:

java -cp stanford-ner.jar edu.stanford.nlp.process.PTBTokenizer

In diesem Fall wollte ich ein Modell bauen, das eine Beschreibung einer einzelnen Zutat enthält, nicht einen vollständigen Satz von Beschreibungen der Zutaten. In der Sprache des BSP bedeutet dies, dass jede Beschreibung der Inhaltsstoffe als ein separates Dokument betrachtet werden muss. Um dies für die NER-Tools von Stanford darzustellen, müssen wir jeden Satz von Tokens durch eine Leerzeile trennen.

Ich habe sie mit Hilfe eines kleinen Shell-Skripts aufgebrochen:

beim Lesen der Zeile; machen Sie

echo $line | java -cp stanford-ner.jar edu.stanford.nlp.process.PTBTokenizer >> train.tok

echo >> train.tok

erledigt < train.txt

Einige Stunden später sahen die Ergebnisse in etwa so aus:

Konditoren NAME

‘ NAME

Zucker NAME

für O

Abstauben O

die O

Kuchen O

1 1/2 MENGE

Becher UNIT

gewürfelt O

geräucherter NAME

lachs NAME

1/2 MENGE

Becher UNIT

ganz O

Mandeln NAME

-LRB- O

3 MENGE

oz EINHEIT

-RRB- O

, O

getoastet O

Nun, da das Trainingsset fertig war, konnte ich das Modell bauen:

java -cp stanford-ner.jar edu.stanford.nlp.ie.crf.CRFClassifier \

-ZugDatei train.tsv \

-serialisierenZu ner-model.ser.gz \

-prop zug.prop

Die Datei train.prop, die ich verwendet habe, war der Beispieldatei der Stanford NER FAQ, austen.prop, sehr ähnlich.

Modellversuch

Eine der Kehrseiten des maschinellen Lernens ist, dass es etwas undurchsichtig ist. Ich wusste, dass ich ein Modell trainiert habe, aber ich wusste nicht, wie genau es sein würde. Glücklicherweise stellt Stanford Testwerkzeuge zur Verfügung, die Ihnen zeigen, wie gut Ihr Modell auf neue Beispiele verallgemeinert werden kann.

Ich nahm etwa 500 weitere Zufallsbeispiele aus meinem Datensatz und durchlief den gleichen faszinierenden Prozess der manuellen Token-Beschriftung. Ich hatte nun eine Reihe von Tests, die ich zur Validierung meines Modells verwenden konnte. Unsere Präzisionsmessungen werden darauf basieren, wie sich die vom Modell erzeugten Token-Etiketten von den Token-Etiketten unterscheiden, die ich von Hand geschrieben habe.

Ich habe das Modell mit diesem Befehl getestet:

Ich habe das Modell mit diesem Befehl getestet:

java -cp stanford-ner.jar edu.stanford.nlp.ie.crf.CRFClassifier \

-loadKlassifikator ner-model.ser.gz \

-testDatei text.tsv

Dieser Testbefehl gibt die Testdaten mit dem Label aus, das ich jedem Token gegeben habe, und dem Label, das das Modell für jedes Token vorhergesagt hat, gefolgt von einer Zusammenfassung der Genauigkeit:

CRFClassifier markierte 4539 Wörter in 514 Dokumenten mit 3953,83 Wörtern pro Sekunde.

Entität P R F1 TP FP FN

NAME 0,8327 0,7764 0,8036 448 90 129

MENGE 0,9678 0,9821 0,9749 602 20 11

EINHEIT 0,9501 0,9630 0,9565 495 26 19

Gesamtzahl 0,9191 0,9067 0,9129 1545 136 159

Die Spaltenüberschriften sind etwas undurchsichtig, aber es handelt sich um Standardmetriken des maschinellen Lernens, die mit ein wenig Erklärung sinnvoll sind.

P ist Präzision: es ist die Anzahl der Token eines bestimmten Typs, die das Modell korrekt identifiziert hat, von der Gesamtzahl der Token, die das erwartete Modell von diesem Typ war. 83% der Token, die das Modell als NAME-Token identifiziert hat, waren tatsächlich NAME-Token, 97% der Token, die das Modell als MENGE-Token identifiziert hat, waren tatsächlich MENGE-Token, usw.

R ist Recall: ist die Anzahl der Token eines bestimmten Typs, die das Modell korrekt identifiziert hat, bezogen auf die Gesamtzahl der Token dieses Typs in der Testmenge. Das Modell fand 78% der NAME-Marken, 98% der MENGE-Marken usw.

F ist das F1-Ergebnis, das Präzision und Rückruf kombiniert. Es ist möglich, dass ein Modell sehr ungenau ist, aber dennoch eine hohe Punktzahl in Bezug auf Genauigkeit oder Neuberechnung aufweist: Wenn ein Modell jedes Token als NAME gekennzeichnet hätte, würde es eine sehr gute Erinnerungspunktzahl erhalten. Die Kombination der beiden als F1-Scores ergibt eine einzige Zahl, die repräsentativer für die Gesamtqualität ist.

TP, FP und FN sind echte Positive, falsche Positive und falsche Negative.

Verwendung des Modells

Jetzt, da ich ein Modell hatte und mir sicher war, dass es einigermaßen genau war, konnte ich es verwenden, um neue Beispiele zu klassifizieren, die nicht im Trainings- oder Testsatz enthalten waren.

Hier ist der Befehl zum Ausführen des Modells:

$ echo “1/2 Tasse Mehl” | \

java -cp stanford-ner/stanford-ner.jar edu.stanford.nlp.ie.crf.CRFClassifier \

-loadKlassifikator ner-model.ser.gz \

-lesenStdin

Aufgerufen am Mittwoch, 27. September 08:18:42 EDT 2017 mit Argumenten: -loadKlassifikator

ner-model.ser.gz -readStdin

loadClassifier=ner-Modell.ser.gz

readStdin=wahr

Laden des Klassifikators aus ner-model.ser.gz … erfolgt [0.3 sec].

1/2/MENGE Becher/Mengeneinheit/O Mehl/Name

CRFClassifier markierte 4 Wörter in 1 Dokument mit 18,87 Wörtern pro Sekunde.

$ echo “1/2 Tasse Mehl” | \

java -cp stanford-ner/stanford-ner.jar edu.stanford.nlp.ie.crf.CRFClassifier \

-loadKlassifikator ner-model.ser.gz \

-readStdin 2>/dev/null

1/2/MENGE Becher/Mehleinheit/ Mehl/Name

Iterieren auf dem Modell

Selbst mit diesen scheinbar hohen F1-Ergebnissen war das Modell nur so gut wie sein Trainingsset. Als ich zurückging und meinen vollständigen Satz von Zutatenbeschreibungen durch das Modell laufen ließ, entdeckte ich schnell einige Mängel.

Das offensichtlichste Problem war, dass das Modell die Unzen Flüssigkeit nicht als Einheit erkennen konnte. Als ich mir das Trainingsset und das Testset noch einmal ansah, gab es kein einziges Beispiel für flüssige Unzen, Fl oz oder Fl oz.

Meine Zufallsstichprobe war nicht groß genug, um die Daten wirklich darzustellen.

Ich wählte zusätzliche Trainings- und Testbeispiele aus und achtete darauf, verschiedene Darstellungen von flüssigen Unzen in meine Trainings- und Testsätze aufzunehmen. Das aktualisierte Modell schnitt bei den aktualisierten Testsätzen ähnlich gut ab und hatte keine Probleme mehr mit flüssigen Unzen.