Grâce aux formats de données intégrés comme le microformat h-recipe et le schéma de recette Schema.org, de nombreuses recettes publiées sur le Web sont marquées sémantiquement. Mieux encore, il existe un joyau de Ruby appelé hangry pour analyser ces formats. En peu de temps, j’ai transformé les recettes en données structurées.

La chose qui m’intéressait le plus était les ingrédients, et là j’ai trouvé mon problème suivant : j’avais des listes d’ingrédients lisibles par l’homme, rien d’assez structuré pour comparer les quantités, trouver des similitudes ou convertir des unités.

Les ingrédients sont difficiles

Les premiers exemples que j’ai examinés m’ont semblé assez simples :

[

“2 cuillères à soupe de beurre”.

“2 cuillères à soupe de farine.”

“1/2 tasse de vin blanc.”

“1 tasse de bouillon de poulet.”

]

Il semblait qu’un schéma clair émergeait, et peut-être qu’une ligne de code Ruby aurait suffi :

quantité, unité, nom = description.split(” “, 3)

Malheureusement, la réalité est beaucoup plus complexe. J’ai trouvé de plus en plus d’exemples qui ne correspondaient pas à ce schéma simple. Certains ingrédients avaient des quantités multiples qu’il fallait combiner (“3 tasses et 2 cuillères”, ou “2 paquets de 10 onces”) ; d’autres avaient des quantités alternatives en métrique et en impérial, soit en tasses et en onces ; d’autres encore suivaient le nom de l’ingrédient avec les instructions de préparation, ou listaient plusieurs ingrédients ensemble dans un même article.

Les cas particuliers s’accumulaient de plus en plus, et mon simple code Ruby s’emmêlait de plus en plus. J’ai cessé de me sentir à l’aise avec le code, puis j’ai cessé de penser qu’il fonctionnerait après avoir été remanié, et j’ai fini par le jeter.

J’avais besoin d’un tout nouveau plan.

Reconnaissance des entités nommées

Cela me semblait être le problème parfait pour l’apprentissage machine supervisé – j’avais beaucoup de données que je voulais classer ; classer manuellement un seul exemple était assez facile ; mais identifier manuellement un schéma général était au mieux difficile, au pire impossible.

Lorsque j’ai envisagé mes options, la reconnaissance d’une entité nommée m’a semblé être l’outil adéquat à utiliser. Les systèmes de reconnaissance d’entités nommées identifient des catégories prédéfinies dans le texte ; dans mon cas, je voulais qu’il reconnaisse leurs noms, quantités et unités d’ingrédients.

J’ai opté pour le NER de Stanford, qui utilise un modèle séquentiel de champs conditionnels aléatoires. Pour être honnête, je ne comprends pas les mathématiques qui se cachent derrière ce type de modèle particulier, mais vous pouvez lire le document1 si vous voulez tous les détails gores. Ce qui était important pour moi, c’était de pouvoir former ce modèle NER sur mon ensemble de données.

Le processus que j’ai dû suivre pour former mon modèle était basé sur l’exemple de Jane Austen tiré de la FAQ sur le NRE de Stanford.

Formation du modèle

La première chose que j’ai faite a été de collecter mes données d’échantillonnage. Dans une même recette, la façon dont les ingrédients sont écrits est assez uniforme. Je voulais m’assurer de disposer d’une bonne gamme de formats, j’ai donc combiné les ingrédients d’environ 30 000 recettes en ligne en une seule liste, je les ai classés au hasard et j’ai choisi les 1 500 premiers pour mon ensemble de formation.

C’était exactement comme ça :

du sucre pour saupoudrer le gâteau

1 tasse et 1/2 tasse de saumon fumé en dés

1/2 tasse d’amandes entières (3 oz), grillées

Ensuite, j’ai utilisé une partie de la suite d’outils de Stanford sur le PNB pour les diviser en copeaux.

La commande suivante permet de lire le texte à partir de l’entrée standard, et les jetons de sortie en sortie standard :

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

Dans ce cas, je voulais construire un modèle qui comprend la description d’un seul ingrédient, et non un ensemble complet de descriptions des ingrédients. Dans le langage du PNB, cela signifie que chaque description des ingrédients doit être considérée comme un document séparé. Pour représenter cela aux outils NER de Stanford, nous devons séparer chaque ensemble de jetons par une ligne blanche.

Je les ai brisés en utilisant un petit script shell :

pendant la lecture de la ligne ; faites

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

echo >> train.tok

fait < train.txt

Quelques heures plus tard, les résultats ressemblaient à ceci :

confiseurs NOM

NOM

sucre NOM

pour O

époussetage O

l’O

gâteau O

1 1/2 QUANTITÉ

tasses UNIT

dés O

fumé NOM

saumon NOM

1/2 QUANTITÉ

tasse UNITE

O entier

amandes NOM

-LRB- O

3 QUANTITÉ

oz UNIT

-RRB- O

, O

toasté O

Maintenant que le kit de formation était terminé, je pouvais construire le modèle :

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

-trainFile train.tsv \

-serializeTo ner-model.ser.gz \

-prop train.prop

Le fichier train.prop que j’ai utilisé était très similaire au fichier d’exemple de la FAQ du NER de Stanford, austen.prop.

Test du modèle

L’un des inconvénients de l’apprentissage machine est qu’il est un peu opaque. Je savais que je formais un modèle, mais je ne savais pas à quel point il serait précis. Heureusement, Stanford fournit des outils de test pour vous permettre de savoir dans quelle mesure votre modèle peut être généralisé à de nouveaux exemples.

J’ai pris environ 500 autres exemples aléatoires dans mon ensemble de données, et j’ai suivi le même processus fascinant d’étiquetage manuel des jetons. J’avais maintenant un ensemble de tests que je pouvais utiliser pour valider mon modèle. Nos mesures de précision seront basées sur la différence entre les étiquettes de jetons produites par le modèle et celles que j’ai écrites à la main.

J’ai testé le modèle à l’aide de cette commande :

J’ai testé le modèle à l’aide de cette commande :

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

-loadClassifier ner-model.ser.gz \

-testFile text.tsv

Cette commande de test produit les données de test avec l’étiquette que j’ai donnée à chaque jeton et l’étiquette du modèle prévu pour chaque jeton, suivie d’un résumé de la précision :

CRFClassifier a étiqueté 4539 mots dans 514 documents à 3953,83 mots par seconde.

Entité P R F1 TP FP FN

NOM 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

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

Les titres des colonnes sont un peu opaques, mais il s’agit de mesures d’apprentissage machine standard qui ont du sens avec un peu d’explication.

P est la précision : c’est le nombre de jetons d’un type donné que le modèle a correctement identifié, sur le nombre total de jetons que le modèle attendu était de ce type. 83% des jetons que le modèle a identifiés comme étant des jetons NOM étaient en fait des jetons NOM, 97% des jetons que le modèle a identifiés comme étant des jetons QUANTITE étaient en fait des jetons QUANTITE, etc.

R est le rappel : c’est le nombre de jetons d’un type donné que le modèle a correctement identifié, sur le nombre total de jetons de ce type dans l’ensemble de test. Le modèle a trouvé 78% des jetons NOM, 98% des jetons QUANTITÉ, etc.

F est le score F1, qui combine précision et rappel. Il est possible qu’un modèle soit très imprécis, mais qu’il obtienne tout de même un score élevé en termes de précision ou de recalcul : si un modèle avait étiqueté chaque jeton comme NOM, il aurait obtenu un très bon score de rappel. En combinant les deux comme scores F1, on obtient un seul chiffre qui est plus représentatif de la qualité globale.

TP, FP et FN sont respectivement des vrais positifs, des faux positifs et des faux négatifs.

En utilisant le modèle

Maintenant que j’avais un modèle et que j’étais sûr qu’il était raisonnablement précis, je pouvais l’utiliser pour classer de nouveaux exemples qui n’étaient pas dans les ensembles de formation ou de test.

Voici la commande pour exécuter le modèle :

$ echo “1/2 tasse de farine” | \

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

-loadClassifier ner-model.ser.gz \

-readStdin

Invoqué le Wed Sep 27 08:18:42 EDT 2017 avec des arguments : -loadClassifier

ner-model.ser.gz -readStdin

loadClassifier=ner-model.ser.gz

readStdin=true

Chargement du classificateur à partir de ner-model.ser.gz … done [0.3 sec].

1/2/QUANTITÉ tasse/UNITE de/O farine/NOM

Le CRFClassifier a marqué 4 mots dans 1 document à 18,87 mots par seconde.

$ echo “1/2 tasse de farine” | \

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

-loadClassifier ner-model.ser.gz \

-readStdin 2>/dev/null

1/2/QUANTITÉ tasse/UNITÉ de/O farine/NOM

Itération sur le modèle

Même avec ces scores apparemment élevés en F1, le modèle n’était pas plus bon que son ensemble d’entraînement. Lorsque je suis retourné et que j’ai passé en revue l’ensemble des descriptions des ingrédients du modèle, j’ai rapidement découvert quelques défauts.

Le problème le plus évident était que le modèle ne pouvait pas reconnaître les onces de liquide en tant qu’unité. Lorsque j’ai regardé le kit d’entraînement et le kit de test, il n’y avait pas un seul exemple d’onces de liquide, de fl oz ou de fl oz.

Mon échantillon aléatoire n’était pas assez grand pour représenter vraiment les données.

J’ai sélectionné d’autres exemples d’entraînement et de test, en prenant soin d’inclure diverses représentations d’onces liquides dans mes ensembles d’entraînement et de test. Le modèle mis à jour a obtenu des résultats similaires sur les ensembles de tests mis à jour et n’a plus eu de problèmes avec les onces liquides.