Le modèle du chapitre 7 tient en trente lignes de scikit-learn. Le reste — la machine qui fabrique les exemples, qui décide de la vérité, qui découpe l'examen, qui mesure et qui sert — c'est le harnais. Cette page le décrit étape par étape, avec le fichier de chaque étape et le mode de panne qu'elle empêche.
La seule idée à retenir : un chiffre ne mesure jamais le modèle seul. Il mesure le couple modèle + harnais. Tant qu'on ne sait pas ce que fait le harnais, on ne sait pas ce que veut dire le score — et sur ce projet, quatre fois de suite, le chiffre publiable mesurait autre chose que ce que je croyais.
de F1 au run-056. L'étiquette venait d'un champ de la base qui, sur certains sites, pointait l'offre elle-même : j'étiquetais des offres comme « pas une offre ».
de F1 obtenus par une régression qui ne voit aucun mot — juste cinq nombres. Tout ce qu'elle réussit est une fuite de mise en page, pas de la compréhension.
points de rappel. Les offres passaient par un robot qui exécute le JavaScript, les non-offres par un simple téléchargement. Le modèle avait appris comment on collecte.
de F1 annoncés pour la régression contre 78,6 au 1,5 B — sauf que le premier était mesuré sur 5 247 pages et le second sur 1 500. Trouvé par un audit du harnais, pas par un contrôle du run.
pages carrières d'entreprise dormaient dans la base sans jamais servir. Un lecteur a envoyé une URL, et le détecteur à 92,7 de F1 s'est trompé.
Les trois premiers ont été trouvés par des contrôles écrits exprès pour me donner tort. Le quatrième a été trouvé par un lecteur — c'est-à-dire par le seul examinateur que le harnais ne contient pas encore.
Onze étapes. Chacune transforme la donnée, et chacune peut fabriquer un faux résultat si on ne la surveille pas.
Un échantillonnage aléatoire par famille de domaines : job boards, racines carrières, sections carrières, sites ordinaires, blogs emploi, agences. La famille est l'unité qui compte : c'est elle qui sera, ou non, montrée à l'entraînement.
Le serveur héberge aussi la préprod du partenaire : il ne télécharge pas 33 000 pages. Seules les adresses partent, et le kernel Kaggle moissonne lui-même en 32 fils. Le jeu de données est supprimé de Kaggle une fois le run terminé.
Le point le plus dangereux de toute la chaîne. Si les exemples positifs et négatifs n'arrivent pas par le même chemin, le modèle apprend le chemin.
html_vers_texte() retire scripts et balises, puis tronque à 6 000 caractères. retirer_adresses() efface ensuite toute URL et tout nom de domaine, parce que la commande est explicite : juger sur le texte seul.
C'est l'étape la plus sous-estimée d'un projet d'apprentissage : d'où vient l'étiquette ? Une page n'entre dans la classe « liste » que si elle pointe au moins 5 liens distincts vers des chemins plus profonds du même hôte ressemblant à des postes, comptés sur le HTML brut avant tout nettoyage.
Une page d'entreprise bourrée de vocabulaire RH sans un seul poste : exactement la forme de ce qui se faisait prendre pour une offre. Zéro lien, donc aucun doute sur l'étiquette — elle rejoint « autre ». Les pages à 1-4 liens, elles, restent dehors.
Une découpe aléatoire classique met des pages du même site des deux côtés : le modèle reconnaît le site, pas la notion. Ici certaines familles entières — sections carrières, blogs emploi, agences, accueils d'entreprise, articles — ne servent qu'à l'examen.
Une régression qui ne voit que la longueur, le nombre de retours à la ligne et les densités de chiffres, de ponctuation et de majuscules. Elle ne peut rien savoir d'une offre d'emploi. Tout ce qu'elle réussit est une fuite de mise en page.
cdi, we are hiring, wir suchen, buscamos, vaga… Deux occurrences suffisent. Si le modèle ne fait pas mieux qu'une expression régulière, il ne sert à rien.
MOTS_OFFRELa faute la plus facile du métier : annoncer « 8,8 % → 2,7 % » alors que les deux chiffres viennent de deux jeux de pages différents. Chaque comparaison publiée ici rejoue les deux modèles sur les mêmes octets.
Le conteneur monte extraction.py au lieu de le recopier : une copie finirait par diverger, et le détecteur jugerait alors un texte extrait autrement que ce qu'il a appris. Le plafond de 6 000 est lu chez l'extracteur lui-même pour que les deux ne dérivent jamais.
1 · Un contrôle qui ne peut que vous donner tort vaut mieux qu'un score. Les deux contrôles du chapitre 7 tiennent en vingt lignes chacun et ils ont retiré 23 points au résultat que j'allais publier. Un score ne se conteste pas tout seul ; un contrôle, si.
2 · L'étiquette est une hypothèse, pas une donnée. Chaque fois que j'ai fait confiance à un champ, à un nom de fichier ou à une intuition sur ce qu'une page « devait » être, ça s'est payé. La règle des 5 liens coûte 12 190 exemples jetés : c'est le prix d'une étiquette dont on peut répondre.
3 · Les chiffres se publient avec leurs défauts. La précision de la classe « liste » est de 30,5 % sur les familles inconnues, et c'est écrit sur la page publique à côté du chiffre qui fait plaisir. Un harnais qui ne sert qu'à produire de bonnes nouvelles n'est pas un harnais, c'est une vitrine.
Il n'a pas de témoin de production permanent. Le contrôle de collecte du run-057 a été fait une fois, à la main, sur 439 offres. Rien ne le rejoue à chaque run — donc rien ne m'avertira si l'écart se creuse à nouveau.
Il n'a pas de jeu de non-régression figé. Les pages qui ont mis un modèle en défaut — celle du Crédit Agricole en tête — devraient former un examen permanent que chaque nouveau run doit repasser. Aujourd'hui elles ne vivent que dans le journal.
Il ne garde pas les prédictions brutes. Les runs 057 et 058 enregistrent leurs scores mais pas les réponses détecteur par détecteur, ni les indices des jeux d'examen — alors que le run-055 le faisait. C'est exactement pour ça que l'erreur « deux populations » ci-dessus a dû être recalculée au lieu d'être simplement re-découpée : pour la régression c'est une minute de processeur, pour le 1,5 B il faudrait rallumer un GPU. La règle avait été écrite après le run-050 ; elle a été désappliquée trois runs plus tard.
Il n'oblige à rien pour les traces. Le jeu de données du run-058 a été supprimé de Kaggle, mais rien dans le dépôt ne le prouve : la sortie de commande n'a pas été consignée, contrairement au run précédent. Une garde qui repose sur ma bonne foi n'est pas une garde.
Il ne surveillait pas les secrets. L'audit a trouvé le mot de passe MongoDB de la préproduction du partenaire en clair dans trois scripts d'export, dont deux que je venais d'ajouter au dépôt. Le dépôt n'a aucun remote — rien n'est sorti de la machine — et le mot de passe vit désormais hors du dépôt. Mais c'est une garde qui manquait, et ce n'est pas le harnais qui l'a vue.
Il n'a pas de lecteur. Les trois premières erreurs de cette page ont été trouvées par du code que j'ai écrit contre moi-même. La quatrième a été trouvée par quelqu'un qui a simplement collé une URL. C'est le contrôle le moins cher et le plus efficace du projet, et il n'est pas automatisable — d'où le banc d'essai public sur la page du détecteur.