Imaginez que vous construisez une application qui analyse les conversations d’une équipe de développement. Vous voulez détecter si le ton général est sain ou s’il y a des signes de stress. Vous utilisez NLTagger d’Apple parce qu’il est disponible, gratuit, fonctionne on-device et ne nécessite pas de serveur. Trois lignes de code et le tour est joué.

Première surprise : la phrase “kill the process and restart the daemon” obtient une note de -0.6. Négatif. Presque hostile. “Fatal error in memory allocation” atteint -0.8. Et “crash report uploaded successfully” — qui est littéralement une bonne nouvelle — obtient -0.4.

Votre outil de gestion du bien-être de l’équipe vient soudainement de déterminer que vos développeurs sont au bord de l’épuisement émotionnel. Alors qu’en réalité, quelqu’un vient juste de redémarrer un service.

Le problème : un modèle entraîné pour un autre monde

NLTagger avec .sentimentScore renvoie une valeur entre -1.0 (très négatif) et 1.0 (très positif). Apple l’a introduit avec iOS 13 / macOS 10.15, basé sur un modèle intégré on-device que l’on ne peut ni personnaliser ni entraîner sur ses propres données.

Pour simplifier : c’est une boite noire qui vous donne un chiffre et vous demande de lui faire confiance.

Le modèle fonctionne raisonnablement bien pour son objectif initial : les reviews de produits, les commentaires sur les réseaux sociaux, ou tout autre texte destiné au grand public où “horrible” signifie mauvais et “fantastique” signifie bon. C’est son domaine. Le problème survient quand on veut l’utiliser ailleurs.

L’expérience : texte technique vs texte quotidien

Faisons un test. Voici un bout de code Swift très simple que vous pouvez exécuter dans un Playground :

import NaturalLanguage

func sentiment(of text: String) -> Double {
    let tagger = NLTagger(tagSchemes: [.sentimentScore])
    tagger.string = text
    let (tag, _) = tagger.tag(
        at: text.startIndex,
        unit: .paragraph,
        scheme: .sentimentScore
    )
    return Double(tag?.rawValue ?? "0") ?? 0
}
---

Passons-lui des phrases courantes qu’un programmeur pourrait utiliser au cours d’une journée :

| Phrase                                   | Score        | Sentiment réel                 |
| --------------------------------------- | ------------ | ------------------------------ |
| "Kill the background process"           | -0.5 ~ -0.7  | Neutre (instruction technique) |
| "Fatal error: index out of range"       | -0.7 ~ -0.9  | Neutre (message du compilateur)|
| "Abort the deployment"                  | -0.4 ~ -0.6  | Neutre (décision opérationnelle)|
| "Destroy the old database"              | -0.5 ~ -0.7  | Neutre (maintenance)           |
| "The crash was caused by a null pointer"| -0.5 ~ -0.8  | Neutre (diagnostic technique)  |
| "Successfully deployed to production"   | +0.3 ~ +0.5  | Positif                        |

Attention à ce détail : cinq des six phrases sont parfaitement normales dans un contexte technique, mais elles sont évaluées comme négatives. La seule phrase positive contient "successfully" — un mot que le modèle identifie car il est courant dans le domaine des _reviews_.

Comparons maintenant avec des phrases appartenant au domaine pour lequel le modèle a été conçu :

| Phrase                                 | Score        | Sentiment réel    |
| ------------------------------------- | ------------ | ----------------- |
| "This product is terrible"            | -0.7 ~ -0.9  | Négatif (correct)|
| "I love this app, it's amazing"       | +0.7 ~ +0.9  | Positif (correct)|
| "The food was okay, nothing special"  | -0.1 ~ +0.1  | Neutre (correct) |

Ici, le modèle réussit parfaitement. Le souci n'est pas que le modèle est mauvais, mais qu'il est **hors contexte**.

## Pourquoi cela arrive : le biais lexical

Le modèle dans `NLTagger` ne comprend pas la sémantique. Il ne fait aucune différence entre "kill a process" comme métaphore technique et "kill a person" comme acte de violence. Pour lui, "kill" est simplement un mot à connotation négative. Point.

On appelle cela un _biais lexical_ — un biais où le modèle attribue une polarité à des mots individuels ou à des combinaisons (`n-grams`) sans tenir compte du contexte spécifique. C'est l'équivalent d’un traducteur automatique qui interpréterait "I'm dying of laughter" comme une urgence médicale.

Le vocabulaire du domaine technique est rempli de mots qui, dans un langage quotidien, sont plutôt négatifs :

- **kill**, **terminate**, **abort**, **destroy** — opérations classiques sur des processus.
- **fatal**, **critical**, **severe** — niveaux dans des journaux système.
- **crash**, **panic**, **fault** — événements système.
- **dead**, **zombie**, **orphan** — états de processus.
- **reject**, **deny**, **block** — contrôle d'accès.
- **break**, **interrupt**, **suspend** — gestion de flux d'exécution.

Un paragraphe avec quelques-unes de ces expressions — un journal d'erreurs, un postmortem, une discussion dans un rapport de pull request — sera évalué comme si quelqu'un écrivait depuis un trou de désespoir.

## Le contexte long ne résout pas le problème ?

On pourrait penser : "en fournissant un paragraphe plus long, le modèle devrait mieux comprendre l’intention". Essayons :

```swift
let technical = """
The daemon was killed after a fatal memory error. \
We restarted the service and the crash did not recur. \
Deployment completed successfully with zero downtime.
"""
print(sentiment(of: technical))
// Résultat typique : -0.3 ~ -0.5

Un paragraphe qui décrit un incident bien résolu. La fin est positive. Pourtant, les mots “killed”, “fatal”, “error” et “crash” pèsent davantage que “successfully” et “completed”. Le modèle ne fait qu’une moyenne lexicale, sans interpréter la narration.

Comparons avec un texte de même longueur, tiré d’un domaine classique :

let review = """
The screen had some dead pixels and the battery was terrible. \
But after the replacement, the product works perfectly. \
I'm very happy with the customer service.
"""
print(sentiment(of: review))
// Résultat typique : +0.1 ~ +0.3

Ici, le modèle perçoit bien que la fin positive compense les remarques négatives du début. Pourquoi ? Parce que des mots comme “happy”, “perfectly” et “works” figurent dans le vocabulaire des reviews que le modèle a rencontrées des millions de fois. Par contre, “successfully” et “completed” ont un poids beaucoup moindre dans son lexique technique.

Quelles alternatives ?

Pour analyser le sentiment dans du texte technique dans l’écosystème Apple, vous avez trois vraies options.

1. Core ML avec un modèle personnalisé (option sérieuse)

Entrainez un modèle avec Create ML basé sur vos propres données. Si vous disposez de journaux, messages Slack, ou commits annotés, vous pouvez développer un modèle .mlmodel capable de traiter spécifiquement votre vocabulaire.

// Entrainement avec Create ML (macOS)
import CreateML

let data = try MLDataTable(contentsOf: trainingDataURL)
let classifier = try MLTextClassifier(
    trainingData: data,
    textColumn: "text",
    labelColumn: "sentiment"
)
try classifier.write(to: modelURL)

C’est plus exigeant en travail, mais le modèle s’exécutera on-device comme NLTagger. À la différence près que vous contrôlez les données d’entraînement.

2. Embeddings + classificateur (option élégante)

Depuis la WWDC23, Apple propose des embeddings contextuels basés sur des transformers via le framework NaturalLanguage. Vous pouvez utiliser NLEmbedding afin de générer des représentations vectorielles de votre texte pour ensuite les classifier. Contrairement au score brut, les embeddings capturent des relations sémantiques — par exemple, “kill process” et “stop process” seront proches dans l’espace vectoriel.

import NaturalLanguage

if let embedding = NLEmbedding.sentenceEmbedding(for: .english) {
    let vector = embedding.vector(for: "Kill the background process")
    // Utiliser le vecteur comme entrée dans un classificateur
}

Vous devrez ajouter un classificateur par-dessus (par exemple un MLClassifier basique), mais la représentation sous-jacente est beaucoup plus riche.

3. Heuristique de domaine (la bidouille fonctionnelle)

Si votre cas d’usage est simple — par exemple, filtrer des messages pour détecter un “ton général” dans une solution interne — un prétraitement avec un dictionnaire lexical technique pourrait suffire :

let technicalNeutral: Set<String> = [
    "kill", "terminate", "abort", "destroy", "fatal",
    "crash", "panic", "dead", "zombie", "orphan",
    "reject", "deny", "block", "error", "fault"
]

func preprocessed(_ text: String) -> String {
    var result = text.lowercased()
    for term in technicalNeutral {
        result = result.replacingOccurrences(of: term, with: "process")
    }
    return result
}

C’est une bidouille, oui. Mais une bidouille explicite que l’on peut auditer et ajuster. Mieux vaut cela qu’une boîte noire qui ne fait pas la différence entre un postmortem et une lettre d’adieu.

La leçon à retenir

NLTagger n’est pas défectueux. Il fait exactement ce pour quoi il a été conçu : analyser le sentiment de texte générique, optimisé pour les applications de l’App Store — reviews, messages, annotations.

L’erreur est de croire que l’analyse de sentiment est une problématique universelle. Elle ne l’est pas. Le sentiment est toujours dépendant du domaine. “Fatal” dans un journal de système est aussi neutre que “salut”. “Fatal” dans une conversation est une alerte majeure. Aucun modèle générique ne fera la distinction sans comprendre le contexte du domaine.

Si votre texte est issu d’un environnement technique — journaux, commits, discussions de développement, tickets — NLTagger vous trompera. Pas par malveillance, mais par ignorance. Et le pire mensonge est celui qui est livré sous la forme d’un Double avec deux décimales de fausse précision.

Avant d’incorporer un modèle de sentiment dans votre pipeline, posez-vous cette question : est-ce que mon texte ressemble à une review sur Amazon ? Si la réponse est non, NLTagger n’est pas fait pour vous. Et si la réponse est oui… vous n’aurez probablement pas besoin d’analyse de sentiment.