Dzięki wbudowanym formatom danych, takim jak mikroformat h-recipe i Schema.org recipe, wiele receptur publikowanych w Internecie jest oznaczonych semantycznie. Co więcej, istnieje Rubinowy klejnot o nazwie hangry, który analizuje te formaty. W krótkim czasie przekształciłem receptury w uporządkowane dane.

Najbardziej interesowały mnie składniki i tutaj znalazłem swój kolejny problem: miałem listę czytelnych dla człowieka składników, nic wystarczająco ustrukturyzowanego, aby porównywać ilości, znajdować podobieństwa lub przeliczać jednostki.

Składniki są trudne

Pierwsze przykłady, na które spojrzałem, wydawały mi się całkiem proste:

[

“2 łyżki masła.”

“2 łyżki mąki”.

“1/2 filiżanki białego wina.”

“1 filiżanka bulionu z kurczaka.”

]

Wydawało się, że wyłania się czysty wzorzec, a może wystarczyłaby linia kodu Rubinowego:

ilość, jednostka, nazwa = description.split(” “, 3)

Niestety, rzeczywistość była znacznie bardziej złożona. Znalazłem coraz więcej przykładów, które nie pasowały do tego prostego wzoru. Kilka składników miało wiele ilości, które musiały być połączone (“3 filiżanki i 2 łyżki” lub “2 opakowania po 10 uncji”); niektóre miały alternatywne ilości w metrycznych i angielskich, albo w filiżankach i uncjach; inne podążały za nazwą składnika z instrukcją przygotowania, lub wymieniły kilka składników razem w tym samym artykule.

Szczególne przypadki kumulowały się coraz wyżej, a mój prosty kod rubinowy stawał się coraz bardziej zagmatwany. Przestałem czuć się komfortowo z tym kodem, potem przestałem czuć, że będzie działał po przerobieniu, a w końcu go wyrzuciłem.

Potrzebowałem zupełnie nowego planu.

Uznanie nazwanych podmiotów

Wydawało mi się to idealnym problemem dla nadzorowanej nauki maszynowej – miałem wiele danych, które chciałem skategoryzować; skategoryzowanie jednego przykładu ręcznie było wystarczająco łatwe; ale ręczne zidentyfikowanie szerokiego wzorca było w najlepszym przypadku trudne, a w najgorszym niemożliwe.

Kiedy rozważałem swoje opcje, rozpoznawanie nazwanych jednostek wydawało mi się właściwym narzędziem do wykorzystania. Rozpoznawcy nazwanych jednostek identyfikują wstępnie zdefiniowane kategorie w tekście; chciałem, aby rozpoznawał ich nazwy, ilości i jednostki składników w moim przypadku.

Zdecydowałem się na Stanford NER, który wykorzystuje model sekwencyjny losowych pól warunkowych. Szczerze mówiąc, nie rozumiem matematyki stojącej za tym konkretnym typem modelu, ale jeśli chcesz poznać wszystkie krwawe szczegóły, możesz przeczytać dokument1. Ważne było dla mnie to, że mogłem trenować ten model NER na moim zbiorze danych.

Proces, który musiałam przejść, aby wytrenować mój model, był oparty na przykładzie Jane Austen z Stanford NER FAQ.

Szkolenie modelu

Pierwszą rzeczą, jaką zrobiłem, było zebranie moich danych o próbkach. W ramach jednej receptury, sposób zapisu składników jest dość jednolity. Chciałam się upewnić, że mam dobry wybór formatów, więc połączyłam składniki z około 30.000 przepisów online w jedną listę, zamówiłam je losowo i wybrałam pierwsze 1500 do mojego zestawu szkoleniowego.

Tak to wyglądało:

cukier do posypania ciasta

1 filiżanka i 1/2 filiżanki pokrojonego w kostkę łososia wędzonego

1/2 filiżanki całych migdałów (3 uncje), tostowych

Następnie, użyłem części pakietu narzędzi GNP Stanforda, aby podzielić je na chipy.

Poniższe polecenie odczyta tekst ze standardowego wejścia, a tokeny wyjściowe na standardowe wyjście:

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

W tym przypadku chciałem zbudować model, który zawierałby opis pojedynczego składnika, a nie kompletny zestaw opisów składników. W języku GNP oznacza to, że każdy opis składników musi być traktowany jako osobny dokument. Aby przedstawić to narzędziom NER Stanforda, musimy oddzielić każdy zestaw żetonów pustą linią.

Złamałem je za pomocą małego skryptu powłoki:

podczas czytania linii; zrobić

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

echo >> train.tok

zrobione < train.txt

Kilka godzin później w vimie, wyniki wyglądały coś w tym stylu:

cukiernicy NAZWA

NAZWISKO

cukier NAZWAWA

dla O

Pylenie O

O

ciastko O

1 1/2 ILOŚĆ

kubki UNIT

pokrojony w kostkę O

wędzony IMIĘ

łosoś NAZWA

1/2 ILOŚĆ

filiżanka UNIT

cały O

migdały Nazwisko

-LRB- O

3 ILOŚĆ

oz UNIA

-RRB- O

, O

tosty O

Teraz zestaw szkoleniowy został ukończony, mogłem zbudować model:

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

-TrainFile train.tsv \

-SerializeTo ner-model.ser.gz \

-prop train.prop

Plik train.prop, którego użyłem, był bardzo podobny do przykładowego pliku Stanford NER FAQ, austen.prop.

Test modelu

Jedną z wad uczenia się maszynowego jest to, że jest ono nieco nieprzejrzyste. Wiedziałem, że trenowałem model, ale nie wiedziałem, jak dokładny będzie. Na szczęście Stanford zapewnia narzędzia testowe, dzięki którym można dowiedzieć się, jak dobrze model może uogólniać na nowe przykłady.

Wziąłem jeszcze około 500 losowych przykładów z mojego zbioru danych i przeszedłem ten sam fascynujący proces ręcznego znakowania żetonów. Miałem teraz zestaw testów, których mogłem użyć do walidacji mojego modelu. Nasze precyzyjne pomiary będą opierać się na tym, jak etykiety na tokenach produkowane przez model różnią się od etykiet na tokenach, które pisałem ręcznie.

Przetestowałem model za pomocą tej komendy:

Przetestowałem model za pomocą tej komendy:

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

-loadClassifier ner-model.ser.gz \

-testFile text.tsv

To polecenie testowe wyświetla dane testowe z etykietą, którą nadałem każdemu żetonowi i etykietą modelu przewidywanego dla każdego żetonu, po czym następuje podsumowanie dokładności:

CRFClassifier oznaczył 4539 słów w 514 dokumentach z prędkością 3953,83 słów na sekundę.

Podmiot P R F1 TP FP FN

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

ILOŚĆ 0,9678 0,9821 0,9749 602 20 11

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

Ogółem 0,9191 0,9067 0,9129 1545 136 159

Nagłówki kolumn są nieco nieprzezroczyste, ale są to standardowe metryki nauki maszynowej, które mają sens przy odrobinie wyjaśnienia.

P to precyzja: jest to liczba żetonów danego typu, które model poprawnie zidentyfikował, z całkowitej liczby żetonów, że oczekiwany model był tego typu. 83% żetonów, które model zidentyfikowany jako tokeny NAME, było w rzeczywistości żetonami NAME, 97% żetonów, które model zidentyfikowany jako tokeny QUANTITY, było w rzeczywistości żetonami QUANTITY itp.

R to recall: jest to liczba prawidłowo zidentyfikowanych przez model tokenów danego typu, spośród całkowitej liczby tokenów tego typu w zestawie testowym. Model znalazł 78% tokenów NAME, 98% tokenów QUANTITY, itp.

F to wynik F1, który łączy w sobie precyzję i wycofanie. Możliwe, że model jest bardzo niedokładny, ale nadal ma wysoki wynik w zakresie dokładności lub przeliczenia: gdyby model oznaczył każdy żeton jako NAME, uzyskałby bardzo dobry wynik wycofania z rynku. Połączenie tych dwóch punktów jako F1 daje w rezultacie jedną liczbę, która jest bardziej reprezentatywna dla ogólnej jakości.

TP, FP i FN są odpowiednio prawdziwymi pozytywami, fałszywymi pozytywami i fałszywymi negatywami.

Korzystając z modelu

Teraz miałem model i pewność, że jest dość dokładny, mogłem użyć go do klasyfikacji nowych przykładów, które nie były w zestawie szkoleniowym lub testowym.

Oto polecenie, aby uruchomić model:

$ echo “1/2 filiżanki mąki” | \

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

-loadClassifier ner-model.ser.gz \

-readStdin

Wywołany w środę 27 września 08:18:42 EDT 2017 z argumentami: -loadClassifier

ner-model.ser.gz -readStdin

loadClassifier=ner-model.ser.gz

readStdin=true

Klasyfikator ładujący z ner-model.ser.gz … wykonany [0.3 sek].

1/2/Ilość kubka/JEDNOSTKA mąki/IEMNOŚĆ

CRFClassifier oznaczył 4 słowa w 1 dokumencie z prędkością 18,87 słów na sekundę.

$ echo “1/2 filiżanki mąki” | \

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

-loadClassifier ner-model.ser.gz \

-readStdin 2>/dev/null

1/2/Ilość kubka/jednostka mąki/nazwa

Iteracja na modelu

Nawet z tymi pozornie wysokimi punktami F1, model był tylko tak dobry jak jego zestaw treningowy. Kiedy wróciłem i przepuściłem przez model pełny zestaw opisów składników, szybko odkryłem pewne wady.

Najbardziej oczywistym problemem było to, że model nie potrafił rozpoznać uncji płynu jako jednostki. Kiedy spojrzałem wstecz na zestaw treningowy i testowy, nie było ani jednego przykładu płynnych uncji, fl oz, czy fl oz.

Moja losowo wybrana próbka nie była wystarczająco duża, aby naprawdę przedstawić dane.

Wybrałem dodatkowe przykłady treningowe i testowe, starając się uwzględnić różne reprezentacje płynnych uncji w moich zestawach treningowych i testowych. Uaktualniony model uzyskał podobne wyniki na uaktualnionych zestawach testowych i nie miał więcej problemów z uncjami płynnymi.