Imagina que estás construyendo una app que analiza conversaciones de un equipo de desarrollo. Quieres detectar si el tono del equipo es sano o si hay señales de estrés. Usas NLTagger de Apple porque está ahí, es gratis, corre on-device, y no necesitas servidor. Tres líneas de código y a volar.
Primera sorpresa: la frase «kill the process and restart the daemon» puntúa -0.6. Negativa. Casi hostil. «Fatal error in memory allocation» se lleva un -0.8. Y «crash report uploaded successfully» — que es literalmente una buena noticia — saca un -0.4.
Tu herramienta de bienestar del equipo acaba de decidir que tus desarrolladores están al borde del colapso emocional. Y lo único que pasó es que alguien reinició un servicio.
El problema: un modelo entrenado para otro planeta
NLTagger con .sentimentScore devuelve un valor entre -1.0 (muy negativo) y 1.0 (muy positivo). Apple lo introdujo en iOS 13 / macOS 10.15, funciona con un modelo on-device integrado en el sistema, y no hay forma de personalizarlo ni de ver con qué datos se entrenó.
Para entendernos: es una caja negra que te da un número y te pide que confíes.
El modelo funciona razonablemente bien para lo que fue diseñado: reviews de productos, comentarios de redes sociales, texto de consumo donde «horrible» significa malo y «fantástico» significa bueno. Ese es su dominio. El problema aparece cuando lo sacas de ahí.
El experimento: texto técnico vs. texto cotidiano
Vamos a probarlo. Esto es Swift puro, lo puedes ejecutar en 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
}
Ahora le pasamos frases que cualquier programador diría en un día normal:
| Frase | Puntuación | Sentimiento real |
|---|---|---|
| «Kill the background process» | -0.5 ~ -0.7 | Neutro (instrucción técnica) |
| «Fatal error: index out of range» | -0.7 ~ -0.9 | Neutro (mensaje del compilador) |
| «Abort the deployment» | -0.4 ~ -0.6 | Neutro (decisión operativa) |
| «Destroy the old database» | -0.5 ~ -0.7 | Neutro (mantenimiento) |
| «The crash was caused by a null pointer» | -0.5 ~ -0.8 | Neutro (diagnóstico) |
| «Successfully deployed to production» | +0.3 ~ +0.5 | Positivo |
Ojo al dato: cinco de seis frases perfectamente normales en un contexto técnico puntúan como negativas. La única positiva es la que contiene «successfully» — una palabra que el modelo reconoce del mundo de las reviews.
Y ahora la comparación con frases del dominio para el que sí fue entrenado:
| Frase | Puntuación | Sentimiento real |
|---|---|---|
| «This product is terrible» | -0.7 ~ -0.9 | Negativo (correcto) |
| «I love this app, it’s amazing» | +0.7 ~ +0.9 | Positivo (correcto) |
| «The food was okay, nothing special» | -0.1 ~ +0.1 | Neutro (correcto) |
Aquí clava las tres. El modelo no es malo. Está fuera de contexto.
Por qué ocurre: el sesgo léxico
El modelo de NLTagger no entiende semántica. No sabe que «kill a process» es una metáfora técnica y «kill a person» es violencia. Para él, «kill» es una palabra con carga negativa. Punto.
Esto se llama lexical bias — el sesgo léxico. El modelo asigna polaridad a palabras individuales (o n-grams cortos) sin entender el contexto del dominio. Es el equivalente a que un traductor automático traduzca «I’m dying of laughter» como una emergencia médica.
El vocabulario técnico está plagado de palabras que en lenguaje cotidiano son negativas:
- kill, terminate, abort, destroy — operaciones normales de procesos
- fatal, critical, severe — niveles de log
- crash, panic, fault — eventos del sistema
- dead, zombie, orphan — estados de procesos
- reject, deny, block — control de acceso
- break, interrupt, suspend — control de flujo
Cualquier párrafo que use tres o cuatro de estas palabras — un log de errores, un postmortem, una discusión en un PR — va a puntuar como si alguien estuviera escribiendo desde el fondo de un pozo.
¿No se arregla con más contexto?
Podrías pensar: «si le paso un párrafo más largo con contexto, el modelo debería entender mejor». Probemos:
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))
// Resultado típico: -0.3 ~ -0.5
Un párrafo que describe un incidente resuelto con éxito. El final es positivo. Pero las palabras «killed», «fatal», «error» y «crash» pesan más que «successfully» y «completed». El modelo promedia carga léxica, no interpreta narrativa.
Compáralo con un texto de la misma longitud sobre un producto:
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))
// Resultado típico: +0.1 ~ +0.3
Aquí el modelo sí capta que el final positivo compensa el principio negativo. ¿La diferencia? «happy», «perfectly» y «works» son palabras que el modelo ha visto millones de veces en reviews. «Successfully» y «completed» no tienen el mismo peso en su vocabulario.
Qué usar en su lugar
Si necesitas análisis de sentimiento en texto técnico dentro del ecosistema Apple, tienes tres opciones reales.
1. Core ML con modelo propio (la opción seria)
Entrena un clasificador con Create ML usando datos de tu dominio. Si tienes logs, mensajes de Slack de tu equipo, o commits etiquetados a mano, puedes crear un modelo .mlmodel que entienda que «kill the process» es neutro.
// Entrenar con 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)
Es más trabajo, pero el modelo corre on-device igual que NLTagger. La diferencia es que tú controlas los datos de entrenamiento.
2. Embeddings + clasificador (la opción elegante)
Desde WWDC23, Apple ofrece embeddings contextuales basados en transformers a través del NaturalLanguage framework. Puedes usar NLEmbedding para obtener representaciones vectoriales del texto y clasificar sobre ellas. A diferencia del sentiment score crudo, los embeddings capturan relaciones semánticas — «kill process» y «stop process» estarán cerca en el espacio vectorial.
import NaturalLanguage
if let embedding = NLEmbedding.sentenceEmbedding(for: .english) {
let vector = embedding.vector(for: "Kill the background process")
// Usar el vector como input de un clasificador
}
Necesitas un clasificador encima (un MLClassifier sencillo), pero la representación base es mucho más rica que un simple score léxico.
3. Heurísticas de dominio (la ñapa que funciona)
Si tu caso es simple — por ejemplo, filtrar mensajes por «tono» en una herramienta interna — a veces lo mejor es un diccionario de términos técnicos que neutralice antes de pasar por el modelo:
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
}
Es una chapuza, sí. Pero una chapuza explícita que puedes auditar y ajustar. Mejor que confiar ciegamente en una caja negra que no distingue un postmortem de una carta de despedida.
La lección de fondo
NLTagger no está roto. Hace exactamente lo que fue diseñado para hacer: análisis de sentimiento de texto genérico, optimizado para el tipo de contenido que Apple espera que procesen las apps de la App Store — reviews, mensajes, notas.
El error es nuestro por asumir que «análisis de sentimiento» es un problema universal. No lo es. El sentimiento depende del dominio. «Fatal» en un log es tan neutro como «hola». «Fatal» en una conversación es alarma máxima. Ningún modelo genérico va a distinguir ambos sin contexto de dominio.
Si tu texto viene de un entorno técnico — logs, commits, chats de desarrollo, tickets — NLTagger te va a mentir. No por malicia, sino por ignorancia. Y la peor mentira es la que viene envuelta en un Double con dos decimales de falsa precisión.
Antes de enchufar un modelo de sentimiento a tu pipeline, hazte una pregunta: ¿mi texto se parece a una review de Amazon? Si la respuesta es no, NLTagger no es tu herramienta. Y si la respuesta es sí… probablemente tampoco necesites análisis de sentimiento.
Read this article in English.



