Благодаря встроенным форматам данных, таким как микроформат h-recipe и схема рецептов Schema.org, многие из рецептов, опубликованных в Интернете, помечены семантически. Еще лучше то, что для анализа этих форматов существует Ruby жемчужина, называемая h-recipe. За короткое время я преобразовал рецепты в структурированные данные.

Больше всего меня интересовали ингредиенты, и здесь я нашел свою следующую проблему: у меня были списки читаемых человеком ингредиентов, ничего структурированного, достаточного для сравнения количеств, нахождения сходства или преобразования единиц.

Ингредиенты сложны

Первые примеры, на которые я смотрел, казались мне довольно простыми:

[

“2 столовые ложки масла”.

“2 столовые ложки муки”.

“1/2 чашки белого вина.”

“1 чашка куриного бульона.”

]

Казалось, что появляется четкий шаблон, и, возможно, строчки кода Ruby было бы достаточно:

количество, единица измерения, имя = описание.split(” “, 3)

К сожалению, реальность была намного сложнее. Я находил все больше и больше примеров, которые не соответствовали этому простому шаблону. Некоторые ингредиенты имели несколько количеств, которые нужно было комбинировать (“3 чашки и 2 ложки”, или “2 пачки по 10 унций”); некоторые имели альтернативные количества в метрическом и империальном, либо в чашках и унциях; другие следовали названию ингредиента с инструкцией по приготовлению, либо перечисляли несколько ингредиентов вместе в одной и той же статье.

Особые случаи накапливались все больше и больше, и мой простой код Рубина становился все более и более запутанным. Я перестал чувствовать себя комфортно с кодом, затем перестал чувствовать, что он будет работать после рефакторинга, и в конце концов выбросил его.

Мне нужен был совершенно новый план.

Распознавание названных сущностей

Это казалось мне идеальной проблемой для машинного обучения под наблюдением – у меня было много данных, которые я хотел распределить по категориям; распределить по категориям один пример вручную было достаточно легко; но вручную определить широкую картину было в лучшем случае сложно, а в худшем – невозможно.

Когда я рассматривал свои варианты, название распознавания объектов казалось правильным инструментом для использования. Устройства распознавания названных объектов идентифицируют предопределенные категории в тексте; я хотел, чтобы они распознавали их названия, количество и единицы ингредиентов в моем случае.

Я выбрал Стэнфордский NER, который использует модель последовательности случайных условных полей. Честно говоря, я не понимаю математику, стоящую за этим конкретным типом модели, но вы можете прочитать документ1, если вам нужны все кровавые детали. Для меня было важно, чтобы я мог обучить эту NER-модель на моем наборе данных.

Процесс, которому я должен был следовать, чтобы обучить свою модель, был основан на примере Джейн Остин из Стэнфордского NER FAQ.

Обучение модели

Первое, что я сделала, это собрала образцы данных. В пределах одного рецепта, способ написания ингредиентов достаточно однороден. Я хотел убедиться, что у меня есть хороший диапазон форматов, поэтому я объединил ингредиенты из около 30.000 рецептов онлайн в единый список, заказал их случайным образом и выбрал первые 1500 для моего набора тренировок.

Это выглядело именно так:

сахар, чтобы выпылить торт

1 стакан и 1/2 стакана нарезанного кубиками копчёного лосося

1/2 стакана цельного миндаля (3 унции), поджаренный

Затем я использовал часть набора инструментов GNP Стэнфорда, чтобы разбить их на чипы.

Следующая команда будет читать текст со стандартного входа, а выходные маркеры – на стандартный выход:

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

В этом случае я хотел построить модель, включающую описание одного ингредиента, а не полный набор описаний ингредиентов. На языке GNP это означает, что каждое описание ингредиентов должно рассматриваться как отдельный документ. Чтобы представить это в инструментах NER Стэнфорда, нам нужно разделить каждый набор маркеров пустой строкой.

Я разбил их с помощью небольшого скрипта оболочки:

во время чтения строки; сделать

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

echo >> train.tok

сделано <поезд.txt

Через несколько часов в Виме результаты выглядели примерно так:

имя кондитеров

‘ ФАМИЛИЯ

имя сахара

для О

удаление пыли O

О

кекс О

1 1/2 КАЧЕСТВО

чашки ЮНИТ

нарезанный в форме кубиков O

копчёное имя

название лосося

1/2 КАЧЕСТВО

чашка ЮНИТ

целая О

миндальное имя

-ЛРБ-О

3 КАЧЕСТВО

унция ЮНИТ

-РРБ-О

, O

поджаренный О

Теперь тренировочный комплект закончен, я могу построить модель:

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

-trainFile поезд.tsv \

-serializeTo ner-model.ser.gz \

-проп-поезд.проп

Файл train.prop, который я использовал, был очень похож на пример Stanford NER FAQ, austen.prop.

Тестирование моделей

Один из недостатков машинного обучения в том, что оно немного непрозрачно. Я знал, что тренировал модель, но не знал, насколько она точна. К счастью, Стэнфорд предоставляет тестовые инструменты, чтобы дать вам знать, насколько хорошо ваша модель может обобщать новые примеры.

Я взял еще около 500 случайных примеров из своего набора данных и прошел через тот же увлекательный процесс ручной маркировки токенов. Теперь у меня есть набор тестов, которые я могу использовать для проверки своей модели. Наши точные измерения будут основаны на том, как этикетки маркеров, произведенные моделью, отличаются от этикеток, которые я написал от руки.

Я протестировал модель, используя эту команду:

Я протестировал модель, используя эту команду:

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

-loadClassifier ner-model.ser.gz \

-testFile text.tsv

Эта тестовая команда выводит тестовые данные с меткой, которую я дал каждому маркеру, и меткой, которую модель предсказала для каждого маркера, с последующим обобщением точности:

CRFClassifier пометил 4539 слов в 514 документах со скоростью 3953,83 слова в секунду.

Сущность P R F1 TP FP FN

ИМЯ 0.8327 0.7764 0.8036 448 90 129

КОЛИЧЕСТВО 0,9678 0,9821 0,9749 602 20 11

ЕДИНИЦА 0.9501 0.9630 0.9565 495 26 19

Итоговый показатель 0.9191 0.9067 0.9129 1545 136 159

Заголовки столбцов немного непрозрачны, но это стандартные метрики машинного обучения, которые имеют смысл с небольшим объяснением.

P – это точность: это количество жетонов данного типа, которое модель правильно идентифицировала, из общего количества жетонов, которое ожидаемая модель имела этого типа. 83% токенов, которые модель идентифицировала как токены ФАМИЛИЯ, на самом деле были токенами ФАМИЛИЯ, 97% токенов, которые модель идентифицировала как токены КАЧЕСТВА, на самом деле были токенами КАЧЕСТВА, и т.д.

R является напоминанием: это количество токенов данного типа, которые модель правильно идентифицировала, из общего количества токенов данного типа в тестовом наборе. Модель нашла 78% токенов “ФАМИЛИЯ”, 98% токенов “КАЧЕСТВО” и др.

F – это результат F1, который сочетает в себе точность и отзыв. Возможно, что модель очень неточна, но все же имеет высокий балл в плане точности или пересчета: если бы модель помещала каждую маркерку как ФАМИЛИЮ, она получила бы очень хороший балл отзыва. Объединение этих двух баллов в баллы F1 приводит к одному номеру, который более репрезентативен с точки зрения общего качества.

TP, FP и FN – это истинные положительные, ложные положительные и ложные отрицательные результаты соответственно.

Используя модель

Теперь у меня была модель, и я был уверен, что она достаточно точна, я мог использовать ее для классификации новых примеров, которых не было в обучающих или тестовых наборах.

Вот команда для запуска модели:

$ эхо “1/2 чашки муки” | \

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

-loadClassifier ner-model.ser.gz \

-readStdin

Призван на ср. 27 сентября 08:18:42 EDT 2017 с аргументами: -Загрузить Классификатор

нер-модель.сер.гз -readStdin

loadClassifier=нер-модель.ser.gz

readStdin=true

Загрузка классификатора из ner-model.ser.gz … выполнена [0.3 сек].

1/2/Чашка КАЧЕСТВА/ОБЪЯВЛЕНИЕ муки/НКАМЕНА

Классификатор CRFC отметил 4 слова в 1 документе со скоростью 18,87 слов в секунду.

$ эхо “1/2 стакана муки” | \

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

-loadClassifier ner-model.ser.gz \

-readStdin 2>/dev/null

1/2/Чашка КАЧЕСТВА/НИТ муки/Название

Итеративно на модели

Даже при таких, казалось бы, высоких оценках Формулы-1, модель была хороша лишь настолько, насколько хороша была ее тренировочная установка. Когда я вернулся и запустил полный набор описаний ингредиентов через модель, я быстро обнаружил некоторые недостатки.

Самой очевидной проблемой было то, что модель не могла распознать унции жидкости как единицу измерения. Когда я оглянулся на тренировочный набор и тестовый набор, не было ни одного примера жидких унций, флэ-унция или флэ-унция.

Мой случайный образец не был достаточно большим, чтобы действительно представлять данные.

Я выбрал дополнительные тренировочные и тестовые примеры, позаботившись о том, чтобы включить различные представления жидких унций в мои тренировочные и тестовые наборы. Обновленная модель набрала аналогичный балл на обновленных тестовых наборах и больше не имела проблем с жидкими унциями.