NLTagger est l’API d’Apple pour l’analyse de sentiment. Elle est intégrée dans iOS et macOS, fonctionne on-device, ne nécessite pas de serveur, et se trouve à trois lignes de code de distance. C’est le premier résultat qu’on trouve en cherchant “sentiment analysis Swift”.

Voici ce qu’elle renvoie pour du texte que n’importe quel développeur écrirait lors d’une journée normale :

MessageNLTaggerRéalité
“delete the temp file”-0.8Instruction neutre
“ok”-0.8Confirmation neutre
“run make test”-0.6Instruction neutre
“commit and push”-0.4Instruction neutre
“great job, thanks!”+1.0Positif (correct)
“this is fucking broken”-1.0Négatif (correct)

L’échelle va de -1.0 (très négatif) à +1.0 (très positif). Selon Apple, “delete the temp file” a presque la même charge émotionnelle que “this is fucking broken”. Et “ok” – la réponse la plus neutre de la langue anglaise – obtient un score de -0.8.

Ces chiffres ne sont pas inventés. Ce sont des résultats reproductibles sur n’importe quel Mac avec macOS 14+.

Comment le reproduire

import NaturalLanguage

let tagger = NLTagger(tagSchemes: [.sentimentScore])
tagger.string = "delete the temp file"
let (tag, _) = tagger.tag(
    at: tagger.string!.startIndex,
    unit: .paragraph,
    scheme: .sentimentScore
)
print(tag?.rawValue ?? "nil")
// "-0.8"

Huit lignes de Swift. Le résultat est déterministe : le même texte produit le même score à chaque exécution, sur chaque appareil. Ce n’est pas une erreur aléatoire. C’est un biais systématique.

Pourquoi cela se produit

NLTagger utilise un modèle entraîné avec du texte grand public : reviews de produits, commentaires de réseaux sociaux, opinions sur des restaurants. Dans ce domaine, il fonctionne raisonnablement bien. “This product is terrible” obtient -0.9 et “I love this app” obtient +0.9. Correct dans les deux cas.

Le problème est que le vocabulaire technique partage des mots avec le vocabulaire émotionnel, mais avec des significations complètement distinctes :

  • kill, terminate, abort – opérations normales sur les processus
  • fatal, critical, panic – niveaux de log
  • crash, dead, zombie – états du système
  • delete, destroy, drop – opérations de données
  • reject, deny, block – contrôle d’accès

Pour un modèle entraîné avec des reviews Amazon, “delete” est destructeur, “kill” est violent, et “fatal” est catastrophique. Il n’a pas le contexte pour savoir que “kill the background process” est aussi émotionnel que “ferme la porte en sortant”.

Ceci s’appelle lexical bias : le modèle assigne une polarité aux mots individuels sans comprendre le domaine. C’est l’équivalent d’un traducteur automatique qui interpréterait “I’m killing it” comme un homicide en cours.

Personne ne s’en est aperçu

Et voici ce qui est intéressant : ce biais n’apparaît dans aucun paper académique. Il y a des centaines d’articles sur l’analyse de sentiment en software engineering – analyse de code reviews, détection de toxicité dans les commits, classification d’issues – mais aucun ne mentionne NLTagger. La communauté NLP l’ignore complètement.

La raison est simple : personne n’utilise NLTagger en production. Les chercheurs utilisent des modèles de Hugging Face. Les entreprises utilisent les APIs d’OpenAI ou Google. NLTagger est ce qu’utilise le développeur indépendant qui cherche une solution rapide pour son app iOS, l’implémente en une après-midi, et ne valide jamais les résultats contre des données réelles.

Cela signifie qu’il y a un nombre inconnu d’apps sur l’App Store qui classifient du texte technique avec un modèle qui pense que “ok” est presque aussi négatif qu’une insulte explicite. Et aucune d’entre elles ne le sait.

Un cas réel : monitoring de sessions de code

Le problème n’est pas théorique. J’ai construit Tokamak, une app macOS qui monitore les sessions Claude Code. Une des fonctionnalités que je voulais ajouter était la détection de frustration : si vous passez deux heures à vous battre avec un bug, l’app devrait pouvoir le détecter par le ton de vos messages.

L’approche évidente était NLTagger. Trois lignes de code, zéro dépendance, fonctionne on-device. Le prototype a marché en 20 minutes.

Et il a classifié “supprime le fichier temporaire et lance les tests” comme -0.7. Une instruction complètement neutre que vous donneriez à un assistant de code. Selon NLTagger, j’étais au bord d’une crise émotionnelle.

Comment nous l’avons résolu : couches déterministes d’abord

La solution n’est pas “utiliser un meilleur modèle” (bien que cela aide aussi). La solution est de ne pas dépendre d’un seul modèle opaque pour tout.

SentimentKit est la librairie que j’ai écrite pour résoudre cela. Elle utilise un pipeline de 4 couches où l’analyse déterministe s’exécute en premier et le ML n’intervient qu’en cas d’ambiguïté :

Message
  │
  ├─ Couche 1: Détecteur de keywords (déterministe, ~20KB)
  │  Dictionnaires curatés de vulgarités, frustration et expressions positives.
  │  8 langues (ES, EN, PT, DE, FR, ZH, JA, KO).
  │
  ├─ Couche 2: Règles VADER (déterministe, ~500KB)
  │  Négation ("not good"), intensificateurs ("very bad"),
  │  MAJUSCULES, ponctuation.
  │
  ├─ Couche 3: CoreML DistilBERT (optionnel)
  │  Seulement pour les messages longs et ambigus.
  │
  └─ Couche 4: LLM scorer (optionnel, nécessite API)
     Seulement pour les cas que les couches précédentes ne résolvent pas.

La clé est que les couches 1 et 2 sont déterministes. Elles produisent le même résultat à chaque fois. Pas de modèle opaque. Vous pouvez auditer chaque dictionnaire, chaque règle. Et le plus important : les commandes techniques neutres obtiennent un score de 0.0, pas -0.8.

import SentimentKit

let analyzer = SentimentAnalyzer()

let result = analyzer.analyze("delete the temp file and run make test")
// result.score = 0.0 -- neutre, comme il se doit

let angry = analyzer.analyze("qu'est-ce que c'est que cette merde, ça marche pas du tout")
// angry.score = -2.0 -- deux détections de vulgarité

VADER : l’outil qui devrait être plus connu

Les règles de la couche 2 sont inspirées de VADER (Valence Aware Dictionary and sEntiment Reasoner), un système créé par C.J. Hutto et Eric Gilbert à Georgia Tech en 2014. VADER est un analyseur de sentiment basé sur des règles, pas sur du ML, qui utilise un dictionnaire de ~7500 mots annotés par des humains avec des scores de valence.

Ce qui est intéressant avec VADER est qu’il gère des modificateurs linguistiques que les modèles basés sur bag of words ignorent :

  • Négation : “not good” inverse la polarité
  • Intensificateurs : “very good” amplifie le score
  • Majuscules : “GOOD” obtient plus de points que “good”
  • Mais : “the food was great BUT the service was terrible” – le but donne plus de poids à la seconde clause

VADER a des limitations (il ne comprend pas le sarcasme, fonctionne mieux en anglais), mais sa transparence est sa plus grande vertu. Quand VADER classe quelque chose mal, vous pouvez ouvrir le dictionnaire, trouver l’entrée, et la corriger. Avec NLTagger, vous ne pouvez qu’hausser les épaules.

L’ironie : Apple a créé le problème et la solution

Depuis macOS 26 / iOS 26, Apple offre le framework Foundation Models, un LLM d’environ 3B de paramètres qui fonctionne on-device. Il est incomparablement meilleur que NLTagger pour l’analyse de sentiment car il comprend le contexte, pas seulement des mots isolés.

Le même Apple qui vous a vendu un classificateur qui pense que “ok” est négatif, vous offre maintenant un modèle de langage qui comprend que “kill the process” est une instruction technique. La solution correcte existait dans le même écosystème ; elle est juste arrivée une décennie en retard.

SentimentKit peut utiliser Foundation Models comme couche 4 (LLM scorer). L’ironie d’utiliser Apple Intelligence pour corriger les erreurs de NLTagger ne m’échappe pas.

Comment Anthropic détecte la frustration (spoiler : regex)

Un fait amusant. Dans le code de Claude Code (le CLI d’Anthropic pour Claude), la détection de frustration de l’utilisateur s’implémente avec des expressions régulières. Pas avec NLTagger. Pas avec un modèle de ML. Avec des regex qui cherchent des motifs comme “this is broken”, “doesn’t work”, “what the”.

C’est une approche brute mais efficace : pas de faux positifs dans les commandes techniques, parce que les motifs sont explicites. Anthropic, l’entreprise derrière l’un des LLMs les plus avancés au monde, utilise des regex pour détecter la frustration. Si cela ne vous dit rien sur l’état de l’analyse de sentiment en production, rien ne le fera.

Golden tests : 144 fixtures et plus

SentimentKit inclut 144 golden messages avec des assertions exactes : 35 en espagnol, 35 en anglais, 20 en portugais, 19 en allemand, 20 en français et 15 en chinois. Les données proviennent de datasets publiés (cardiffnlp/tweet_sentiment_multilingual, sepidmnorozy/Chinese_sentiment) et de code reviews réelles de dépôts publics.

Chaque fixture a une plage attendue de score, les expressions qui doivent être détectées, et celles qui ne doivent pas l’être. Le système de tests inclut la détection PHANTOM (entrées du dictionnaire qui ne correspondent jamais aux données réelles) et la détection UNCONSUMED (expressions réelles qui manquent dans les dictionnaires). C’est la même approche que j’utilise dans Tokamak pour valider les DTOs contre les données de production : si le LLM a inventé une donnée, le test échoue.

Testez-le

git clone https://github.com/frr149/SentimentKit
cd SentimentKit
swift test

Ou comme dépendance Swift Package Manager :

.package(url: "https://github.com/frr149/SentimentKit.git", from: "1.0.0")

Le package fonctionne sans CoreML et sans connexion internet. Les couches déterministes (keywords + VADER) suffisent pour la plupart des cas d’usage dans le texte technique.

La leçon

Si vous utilisez NLTagger pour du texte qui ne sont pas des reviews de produits, vous obtenez des données inutilisables avec deux décimales de fausse précision. Le modèle n’est pas cassé ; il est hors domaine. Et l’API ne vous prévient pas.

La communauté NLP l’ignore. Apple ne le documente pas comme limitation. Et les développeurs qui l’utilisent en production ne valident jamais les résultats, parce qu’un Double entre -1.0 et 1.0 semble rigoureux même s’il ne signifie rien.

Avant de faire confiance à un score de sentiment, il faut se demander : “ok” devrait-il être -0.8 ? Si votre outil dit que oui, le problème n’est pas chez l’utilisateur. Il est dans l’outil.