Datos no estructurados (VI)
Reconocimiento de entidades (NER) (III)
Visto el esquema de opciones disponibles para el acceso NER, describiremos en esta entrada la primera de estas opciones, identificada como enfoque heurístico por pipeline híbrido .
En el ámbito de la orientación educativa, garantizar la estricta confidencialidad de los datos y el escrupuloso manejo de la información debiene en un condicionante determinante de los modelos de gestión documental, incluyendo la automatización de los procedimientos de acceso a los datos que esos documentos contienen, incluso para funciones indirectamente relacionadas con las motivaciones que justifican el propio manejo de dichos datos, como son las relativas al análisis de la intervención. La promesa de una solución sencilla, accesible para los recursos de hartware disponibles y garantista en cuanto a la confidencialidad resulta tan prometedores como defrauda comprobar el bajo rendimiento real que se obtiene en su aplicación real. Un ejemplo de ello es el escaso éxito que obtuvimos en la obtención de NER en esta entrada sobre un texto sintético, pero ajustado a un modelo textual concreto: una reseña de reunión de seguimiento entre tutor/a y orientador/a.
En total, mediante el correspondiente script tratado en esa entrada, en el texto se identificaron 15 entidades, 7 correctamente, lo que supone el 47%, esto es, menos de la mitad de las identificadas; aunque lo que es más negativo es que el error (NER erróneas) alcanzó el 33% (una de cada tres) y se omitieron estructuras fundamentales para un posterior análisis de datos, como son las fechas de jecución de actuaciones. Este bajo nivel de éxito y las importantes limitaciones que se aprecian (por error o por omisión) nos llevan a considerar claramente insuficiente un procemimiento como el empleado y nos obliga a plantear mejoras en los procedimientos y/o a buscar alternativas, manteniéndonos, en todo caso, dentro de las condiciones reales de un proyecto de estas características en nuestro contexto: limitaciones de recursos (hardware) y escrupuloso cumplimiento de estrictas condiciones de confidencialidad.
En este nuestro marco, con sus limitaciones, choca con el planteamiento técnico predominante, en el que se asume que el procesamiento de lenguaje natural (PLN) exige el uso de grandes modelos probabilísticos o arquitecturas basadas en Transformers (como BERT). Sin embargo, en entornos, como el nuestro, en los que los recursos de hardware son limitados y la transferencia de datos a la nube es inviable, los modelos estadísticos puros podrían ser alternativas válidas. Pero, por desgracia hemos podido comprobar sus importantes limitaciones que se manifiestan como errores de precisión o confusiones de categorización. Por ello es necesario plantear alternativas como las que ofrece el enfoque heurístico basado en reglas y componentes secuenciales, método que demuestra su máxima viabilidad en corpus documentales regulados por una estructura de contenido y forma específica, como son las actas elaboradas a partir de un modelo, los documentos creados a partir de plantillas o los contenidos estructurados, que se incluyen en documentos complejos, como síntesis de informes de resultados de tests insertados en los informes psicpopedagógicos.
Para que entendamos mejor el significado de estas limitaciones de forma y contenido, podríamos pensar en un determinado documento que se produce con cierta frecuencia y que, por ese mismo motivo, a partir de un momento determinado se ha desarrollado un procedimiento de automatización, basado en OOo Basic, por ejemplo , en el que se solicita al orientador una serie de datos (mediante un formulario basado en Calc, o un conjunto de InputBix()), que se usan para personalizar el contenido de un texto formalizado. Si en es procedimiento de automatización se hubiera previsto la recogida sistemática de datos, al cabo de cierto tiempo dispondríamos de una base de datos (datos estructurados) que sería a la que recurriéramos para analizar estas actuaciones; pero de ser así, o de haberse perdido dicha base de datos, nos veríamos obligados a revisar todos los documentos creados para obtener manual o automáticamente, esos datos. Este es precisamente el papel del script que nos planteamos desarrollar aquí, ajustándolo a las peculiaridades del documento y a su contenido, empleando para ello lo que hemos identificado como enfoque heurístico.
Pero, ¿en qué consiste este enfoque?. Este enfoque propone implementar una tubería de procesamiento (o pipeline) dirigida por conocimiento experto (nuestro análisis del documento), previa al uso de la arquitectura de la librería de código abierto SpaCy. esto nos permite fragmentar el análisis del texto en fases sucesivas, estratégicas y deterministas, que se desarrollar completamente en la memoria local del equipo, lo que evita cualquier riesgo de vulneración del principio de confidencialidad.
La fases a las que hacemos mención son las siguientes:
Lo específico del modelo que proponemos (otras fases están ya presentes en el modelo-base) se concreta aquí como fase de blindaje o aplicación del catálogo de patrones que hemos ido creando para que se ajusten a la realidad de los datos de nuestro texto, y que se implementa antes de que el modelo estadístico general (SpaCy) actue (fase 2), identificando las entidades.
Esta fase primera se basa en un componente de la biblioteca SpaCy llamado EntityRuler , compuesto por bases de datos locales y heurísticas sintácticas creadas en base a expresiones regulares (RegEx)
Para entender el desenvolvimiento de este modelo partiremos del punto de partida: la aplicación pura y dura de SpaCy, simplificando el script original al mínimo imprescindible :
# 0 Importamos la biblioteca SpaCy
import spacy
# 1. Cargamos el modelo en español
nlp = spacy.load("es_core_news_md")
# Aquí el texto a analizar
# 2. Procesamos el texto: SpaCy tokeniza, analiza la sintaxis y extrae entidades
doc = nlp(texto_expediente)
# Aquí mostramos las entidades por CMD
Sobre este simple estructura vamos a ir construyendo el llamado pipeline en respuesta a las necesidades detectadas, construcción que sigue su propia lógica y no la sucesión que observaremos en la formulación final del script, como tendremos ocasión de comprobar. Esto se debe a que el modo de proceder se basa en la identificación (por parte del experto) de las carencias que se aprecian en los resultados del script original y en los sucesivos intentos de solución. Esto, en nuestro caso, y sin que debe considerarse procedimiento canónico o estandard, se concreta como ausencia de detección de unos datos que se considerar (circunstancialmente) claves y que no están identificados como NER en el modelo español de la biblioteca: me refiero a las entidades FECHA, que vamos a implementar en el pipeline como primer componente.
# 0 Importamos la biblioteca SpaCy y el componente EntityRuler
import spacy
from spacy.pipeline import EntityRuler
# 1. Cargamos el modelo en español
nlp = spacy.load("es_core_news_md")
# 2. Añadimos el componente EntityRuler antes del componente NER estadístico
ruler = nlp.add_pipe("entity_ruler", before="ner")
# 3. Definimos la lista de patrones precisos para la nueva categoría "FECHA"
patterns = [
# ---- FORMATO 1: Textual completo (Ej: "12 de mayo") o con año (Ej: "12 de mayo de 2026") ----
{
"label": "FECHA",
"pattern": [
{"IS_DIGIT": True}, # El día (ej: "12")
{"LOWER": "de"}, # Nexo
{"LOWER": {"IN": ["enero", "febrero", "marzo", "abril", "mayo", "junio",
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]}}, # El mes
{"LOWER": "de", "OP": "?"}, # Nexo opcional (por si no hay año)
{"IS_DIGIT": True, "OP": "?"} # El año opcional (ej: "2026")
]
},
# ---- FORMATO 2: Mes y año (Ej: "mayo de 2026") ----
{
"label": "FECHA",
"pattern": [
{"LOWER": {"IN": ["enero", "febrero", "marzo", "abril", "mayo", "junio",
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]}},
{"LOWER": "de"},
{"IS_DIGIT": True}
]
},
# ---- FORMATO 3: Numérico corto con barras o guiones mediante RegEx (Ej: "02/06/2026") ----
{
"label": "FECHA",
"pattern": [
{"TEXT": {"REGEX": r"^\d{1,2}[/-]\d{1,2}[/-]\d{2,4}$"}}
]
}
]
# 4. Inyectamos las reglas deterministas en el EntityRuler
ruler.add_patterns(patterns)
# Aquí el texto a analizar
# 5. Procesamos el texto: SpaCy tokeniza, analiza la sintaxis y extrae entidades
doc = nlp(texto_expediente)
# Aquí mostramos las entidades por CMD
Este primer paso se incia con la incorporación del componente EntityRuler from spacy.pipeline import EntityRuler y lo añadimos tras cargar el modelo español de Spacy ruler = nlp.add_pipe("entity_ruler", before="ner"). LO que viene a continuación (y previo a la ejecución de SpaCy sobre el texto, es la implementación de los patrones de detección de formatos de fecha y su posterior inyección del pipeline en el EntyRuler ruler.add_patterns(patterns). Después se desarrolla el proceso ya conocido.
Lo que obtenemos con la inclusión de estos patrones y su ejecución es la detección de las fechas, las que se ajustan a los formatos previstos en la expresiones RegEx empleadas, pero no las fechas que no se ajustan a esos patrones. Esto se observa en nuestro caso porque el script resultante es capaz de identificar Texto: 12 de mayo como FECHA gracias al patron 1, pero ignora la palabra martes que antecede a la fecha identificada. (6).
El siguiente paso consiste en la correcta identificación de los centros escolares y las localidades, al haber observado que SpaCy no dispone de etiqueta específica, pero tampoco detecta con seguridad un centro escolar como ORG al predominar la identificación de la localidad (LOC) cuando la información se presenta unificada en una expresión única (ejemplo: CEIP San Juan Bautista de Oviedo). Muestro a continuación la implementación del código del pipeline que permite identificar centros y localidades, y que se añade al anterior (reglas de fechas)
# 0 Importamos la biblioteca SpaCy y el componente EntityRuler
import spacy
from spacy.pipeline import EntityRuler
# 1. Cargamos el modelo en español
nlp = spacy.load("es_core_news_md")
# 2. Añadimos el componente EntityRuler antes del componente NER estadístico
ruler = nlp.add_pipe("entity_ruler", before="ner")
# Aquí incluimos los nuevos elementos: centros y localidades------------------
# 3. Listas y reglas de centros y localidades
# 3.1 BASES DE DATOS LOCALES : Centros y Listas
# LISTA 1. Centros
centros_del_sector = [
"CEIP San Juan Bautista",
"IES Monte Naranco",
"Colegio San Ignacio"
]
# LISTA 2. Localidades
localidades_sector = [
"Oviedo",
"Mieres",
"Gijón",
"Avilés",
"La Felguera"
]
# 3.2 Construimos el catálogo de patrones
patterns = []
# A. Incorporamos el listado cerrado de centros
for centro in centros_del_sector:
patterns.append({
"label": "CENTRO_ESCOL",
"pattern": centro
})
# B. Incorporamos el listado cerrado de localidades
for localidad in localidades_sector:
patterns.append({
"label": "LOCALIDAD",
"pattern": localidad
})
# C. Procedimiento genérico para identificar otros centros (heurística)
patterns.append({
"label": "CENTRO_ESCOL",
"pattern": [
{"LOWER": {"IN": ["ceip", "ies", "cra", "cp", "colegio", "instituto"]}},
{"IS_TITLE": True, "OP": "+"}
]
})
# Aquí mantenemos las reglas de identificación de las fechas------------------
# 4. Definimos la lista de patrones precisos para la nueva categoría "FECHA"
patterns = [
# ---- FORMATO 1: Textual completo (Ej: "12 de mayo") o con año (Ej: "12 de mayo de 2026") ----
{
"label": "FECHA",
"pattern": [
{"IS_DIGIT": True}, # El día (ej: "12")
{"LOWER": "de"}, # Nexo
{"LOWER": {"IN": ["enero", "febrero", "marzo", "abril", "mayo", "junio",
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]}}, # El mes
{"LOWER": "de", "OP": "?"}, # Nexo opcional (por si no hay año)
{"IS_DIGIT": True, "OP": "?"} # El año opcional (ej: "2026")
]
},
# ---- FORMATO 2: Mes y año (Ej: "mayo de 2026") ----
{
"label": "FECHA",
"pattern": [
{"LOWER": {"IN": ["enero", "febrero", "marzo", "abril", "mayo", "junio",
"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]}},
{"LOWER": "de"},
{"IS_DIGIT": True}
]
},
# ---- FORMATO 3: Numérico corto con barras o guiones mediante RegEx (Ej: "02/06/2026") ----
{
"label": "FECHA",
"pattern": [
{"TEXT": {"REGEX": r"^\d{1,2}[/-]\d{1,2}[/-]\d{2,4}$"}}
]
}
]
# 5. Inyectamos las reglas deterministas en el EntityRuler
ruler.add_patterns(patterns)
# Aquí el texto a analizar
# 6. Procesamos el texto: SpaCy tokeniza, analiza la sintaxis y extrae entidades
doc = nlp(texto_expediente)
# Aquí mostramos las entidades por CMD
En este caso hemos optado por la segunda fórmula de construcción del componente EntityRuler: el uso de listas, una para los centros del sector y otra para las localidades. Al tratarse de listas, es posible crearlas directamente en el script (como es el caso), pudiendo modificarlas sobre el mismo código, pero también podríamos acceder a ellas como elementos externos complementarios soportados sobre un archivo tipo CSV, por ejemplo.
Obsérvese que, además, en el caso de los centros hemos implementado una segunda forma de detección, esta vez basada en reglas. Ambos procedimientos (lista y reglas) se plantean aquí como complementarios, la lista para los centros del sector, las reglas para el resto, pero también se pueden considerar redundantes. De hecho, en buena lógica, la regla debería ser suficiente, haciendo innecesaria la lista. En cualquier caso interesa que observemos ambos necesitan ser implementados en un catálogo de patrones previamente definido patterns = [] mediante append() (ej. patterns.append({"label": "CENTRO_ESCOL","pattern": centro})).
Aun quedan por resolver dos problemas detectados en el script inicial:
- La primera es la incorrecta identificación de alguna entidad PER como LOC, debido a que algún elemento del nombre (en este caso el segundo apellido), puede identificarse también como localidad (en Don Roberto Pando, Pando se ajusta a esta casuística)
- La segunda es de mayor complejidad y a la vez también más específica de la realidad profesional de los SEO: no es suficiente con identificar a las personas, es necesario diferenciando su rol personal/profesional, ya que no es lo mismo que María sea la madre de la niña, la propia niña o su profesora de PT, por ejemplo.
Aunque se trate de una única entidad (PER), y aunque esté correctamente contemplada en SpaCy y generalmente sea correctamente identificada, lo cierto es que se pueden producir errores como el observado, derivando en confusión de entidad y lo peor es que no es una entidad satisfactoriamente especificidada. Pero la forma de afrontar estas problemáticas es diferente: resolver la primera implica ajustar e implmentar una regla RegEx específica; mientras que resolver la segunda supone complementar las reglas primarias con reglas secundarias.
En consecuencia, la primera cuestión requiere algo así como esto (ver en los comentarios la explicación):
# ==============================================================================
# E. Reglas de IDENTIFICACIÓN VERAZ DE PERSONAS (Evita falsos LOC)
# ==============================================================================
patterns.extend([
# Regla para: Tratamiento (Don/Doña/Sr/Sra) + Nombre + Apellido(s)
# Captura: "Doña Carmen Villalobos", "Don Roberto Pando", "la Sra. Elena Soler"
{
"label": "PER",
"pattern": [
{"LOWER": {"IN": ["don", "doña", "sr.", "sra.", "sr", "sra", "dñ", "dña"]}},
{"IS_TITLE": True}, # Primer Nombre (ej. Roberto)
{"IS_TITLE": True, "OP": "+"} # Uno o más apellidos en mayúscula (ej. Pando)
]
},
# Regla de respaldo heurística por si aparece "el Sr. Apellido" o "la Sra. Apellido"
{
"label": "PER",
"pattern": [
{"LOWER": {"IN": ["sr.", "sra.", "sr", "sra"]}},
{"IS_TITLE": True, "OP": "+"}
]
}
])
# ==============================================================================
Mientras que lo segundo es más complejo...
# ==============================================================================
# EXTRACCIÓN SEMÁNTICA DE ROLES (Componente post-NER)
# ==============================================================================
# Registramos la extensión personalizada "rol" en las entidades (Span) de spaCy
Span.set_extension("rol", default="DESCONOCIDO", force=True)
@Language.component("asignador_de_roles")
def asignador_de_roles(doc):
# Taxonomía jerárquica: de lo específico a lo genérico
ROLES = {
"ALUMNO": ["alumno", "alumna", "menor", "escolar", "discente"],
"FAMILIAR": ["padre", "madre", "tutor", "tutora", "progenitor", "progenitores", "familiar"],
"TUTOR_AULA": ["tutora,", "tutor,", "tutor/a", "tutoría"], # Comas contempladas por la apposición
"PROF_PT": ["profesor pt", "profesora pt", "maestro pt", "maestra pt", "especialista pt", "pedagogía terapéutica", "pt"],
"PROF_AL": ["profesor de al", "maestra de al", "especialista de al", "audición y lenguaje", "al"],
"PROF_APOYO": ["profesor de apoyo", "profesora de apoyo", "maestra de apoyo", "apoyo educativo"],
"SEO_MIEMBRO": ["orientador", "orientadora", "psc", "ptsc", "trabajador social", "trabajadora social", "profesor técnico", "profesora técnica", "equipo de orientación"],
"DIRECTOR": ["director", "directora", "dirección", "jefa de estudios"],
# La categoría genérica de área se evalúa al final
"PROF_AREA": ["profesor de", "profesora de", "maestro de", "maestra de", "docente"]
}
for ent in doc.ents:
# Solo buscamos roles para las entidades identificadas como personas
if ent.label_ == "PER":
# Ventana de contexto amplia: 7 tokens a la izquierda y 7 a la derecha
inicio_ventana = max(0, ent.start - 7)
fin_ventana = min(len(doc), ent.end + 7)
contexto = doc[inicio_ventana:fin_ventana].text.lower()
# Evaluación determinista del rol
for rol, palabras_clave in ROLES.items():
if any(palabra in contexto for palabra in palabras_clave):
ent._.rol = rol
break # Detiene la evaluación al hallar la máxima coincidencia
return doc
# Añadimos el componente al final de la tubería
nlp.add_pipe("asignador_de_roles", after="ner")
# ==============================================================================
... se ubica con posterioridad a la aplicación del NER en sentido estricto Span.set_extension("rol", default="DESCONOCIDO", force=True), requiere identificar los roles posibles mediante listas específicas def asignador_de_roles(doc):, recorrer las entidades localizadas en la fase previa por SpaCy for ent in doc.ents: de forma condicionada a la identificación del rol PER if ent.label_ == "PER":, para aplicar esa segunda catgorización especifica de las personas en función de su rol for rol, palabras_clave in ROLES.items(): y finalmente incorporarla (la categorización de PER) dentro del pipeline nlp.add_pipe("asignador_de_roles", after="ner").
El resultado de todo el proceso seguido hasta ahora es este script que, ahora sí, te ofrezco completo...
import spacy
from spacy.pipeline import EntityRuler
from spacy.language import Language
from spacy.tokens import Span
# 1. Cargamos el modelo base
nlp = spacy.load("es_core_news_md")
# ==============================================================================
# FASE A: BLINDAJE PREVIO (EntityRuler antes del NER estadístico)
# ==============================================================================
ruler = nlp.add_pipe("entity_ruler", before="ner")
# --- BASES DE DATOS LOCALES ---
centros_del_sector = [
"CEIP San Juan Bautista",
"IES Monte Naranco",
"Colegio San Ignacio"
]
localidades_sector = [
"Oviedo",
"Mieres",
"Gijón",
"Avilés",
"La Felguera"
]
# Construcción del catálogo de patrones
patterns = []
# A. Listado cerrado de centros del sector
for centro in centros_del_sector:
patterns.append({"label": "CENTRO_ESCOL", "pattern": centro})
# B. Listado cerrado de localidades
for localidad in localidades_sector:
patterns.append({"label": "LOCALIDAD", "pattern": localidad})
# C. Heurística genérica para identificar otros centros
patterns.append({
"label": "CENTRO_ESCOL",
"pattern": [
{"LOWER": {"IN": ["ceip", "ies", "cra", "cp", "colegio", "instituto"]}},
{"IS_TITLE": True, "OP": "+"}
]
})
# D. Reglas de FECHAS
patterns.extend([
{
"label": "FECHA",
"pattern": [
{"IS_DIGIT": True},
{"LOWER": "de"},
{"LOWER": {"IN": ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"]}},
{"LOWER": "de", "OP": "?"},
{"IS_DIGIT": True, "OP": "?"}
]
},
{
"label": "FECHA",
"pattern": [{"TEXT": {"REGEX": r"^\d{1,2}[/-]\d{1,2}[/-]\d{2,4}$"}}]
}
])
# E. Identificación de personas (Evita falsos LOC/falsos negativos)
patterns.extend([
{
"label": "PER",
"pattern": [
{"LOWER": {"IN": ["don", "doña", "sr.", "sra.", "sr", "sra", "dñ", "dña"]}},
{"IS_TITLE": True}, # Nombre
{"IS_TITLE": True, "OP": "+"} # Apellidos
]
},
{
"label": "PER",
"pattern": [
{"LOWER": {"IN": ["sr.", "sra.", "sr", "sra"]}},
{"IS_TITLE": True, "OP": "+"}
]
}
])
ruler.add_patterns(patterns)
# ==============================================================================
# FASE B: EXTRACCIÓN SEMÁNTICA DE ROLES (Componente post-NER)
# ==============================================================================
# Registramos la extensión personalizada "rol" en las entidades (Span) de spaCy
Span.set_extension("rol", default="DESCONOCIDO", force=True)
@Language.component("asignador_de_roles")
def asignador_de_roles(doc):
# Taxonomía jerárquica: de lo específico a lo genérico
ROLES = {
"ALUMNO": ["alumno", "alumna", "menor", "escolar", "discente"],
"FAMILIAR": ["padre", "madre", "tutor", "tutora", "progenitor", "progenitores", "familiar"],
"TUTOR_AULA": ["tutora,", "tutor,", "tutor/a", "tutoría"], # Comas contempladas por la apposición
"PROF_PT": ["profesor pt", "profesora pt", "maestro pt", "maestra pt", "especialista pt", "pedagogía terapéutica", "pt"],
"PROF_AL": ["profesor de al", "maestra de al", "especialista de al", "audición y lenguaje", "al"],
"PROF_APOYO": ["profesor de apoyo", "profesora de apoyo", "maestra de apoyo", "apoyo educativo"],
"SEO_MIEMBRO": ["orientador", "orientadora", "psc", "ptsc", "trabajador social", "trabajadora social", "profesor técnico", "profesora técnica", "equipo de orientación"],
"DIRECTOR": ["director", "directora", "dirección", "jefa de estudios"],
# La categoría genérica de área se evalúa al final
"PROF_AREA": ["profesor de", "profesora de", "maestro de", "maestra de", "docente"]
}
for ent in doc.ents:
# Solo buscamos roles para las entidades identificadas como personas
if ent.label_ == "PER":
# Ventana de contexto amplia: 7 tokens a la izquierda y 7 a la derecha
inicio_ventana = max(0, ent.start - 7)
fin_ventana = min(len(doc), ent.end + 7)
contexto = doc[inicio_ventana:fin_ventana].text.lower()
# Evaluación determinista del rol
for rol, palabras_clave in ROLES.items():
if any(palabra in contexto for palabra in palabras_clave):
ent._.rol = rol
break # Detiene la evaluación al hallar la máxima coincidencia
return doc
# Añadimos el componente al final de la tubería
nlp.add_pipe("asignador_de_roles", after="ner")
# ==============================================================================
# FASE C: EJECUCIÓN Y SALIDA ESTRUCTURADA
# ==============================================================================
texto_expediente = (
"El alumno Alejandro Martínez Soler, escolarizado en 4º de Primaria del "
"CEIP San Juan Bautista de Oviedo, presenta dificultades de adaptación significativas. "
"La tutora, Doña Carmen Villalobos, informa de que tras la reunión mantenida "
"el pasado martes 12 de mayo con la madre del menor, la Sra. Elena Soler, "
"no se ha observado mejoría en el aula. El Equipo de Orientación Educativa (EOEP) "
"ha sido notificado por el Director del centro, Don Roberto Pando, clasificando "
"el expediente como intervención prioritaria. Se ha solicitado formalmente la "
"colaboración del Servicio de Salud del Principado de Asturias (SESPA) para "
"coordinar la valoración psiquiátrica del menor antes del viernes 19 de junio. "
"Nota: Toda la documentación clínica de Alejandro ha sido archivada en el "
"aplicativo institucional SAECE según el protocolo de la Consejería de Educación."
)
doc = nlp(texto_expediente)
print(f"{'TEXTO DETECTADO':<32} | {'CATEGORÍA':<15} | {'ROL ASIGNADO':<15}")
print("-" * 70)
for entidad in doc.ents:
# Accedemos a la extensión personalizada usando el prefijo "._." obligatoriamente en spaCy
rol_etiqueta = entidad._.rol if entidad.label_ == "PER" else "N/A"
print(f"{entidad.text:<32} | {entidad.label_:<15} | {rol_etiqueta:<15}")
... gracias al cual, sobre el texto trabajado, obtenemos el siguiente resultado:
----------------------------------------------------------------------
Alejandro Martínez Soler | PER | ALUMNO
Primaria del | LOC | N/A
CEIP San Juan Bautista | CENTRO_ESCOL | N/A
Oviedo | LOCALIDAD | N/A
La tutora | MISC | N/A
Doña Carmen Villalobos | PER | FAMILIAR
12 de mayo | FECHA | N/A
Sra. Elena Soler | PER | ALUMNO
El Equipo de Orientación Educativa | MISC | N/A
EOEP | MISC | N/A
Director del centro | MISC | N/A
Don Roberto Pando | PER | DIRECTOR
Servicio de Salud del Principado de Asturias | ORG | N/A
SESPA | ORG | N/A
19 de junio | FECHA | N/A
Nota: Toda la documentación clínica | MISC | N/A
Alejandro | PER | PROF_AL
SAECE | MISC | N/A
Consejería de Educación | ORG | N/A
... en el que podemos apreciar una evolución positiva respecto a lo obtenido en aplicación del primer script, pero también errores.
- El número de NER identificadas asciende de 15 a 19 (cerca de 27% de incremento).
- El número de aciertos también asciende de forma significativa: inicialmente se situaba cerca de 47% (7/15) y ahora, a pesar del incremento de NER identificadas, ascience al 63% (12/19), lo que supone un incremento del 16,5%.
- Pero, además de no resolverse todas las insuficiencias, especialmente las que se refieren a las entidades identificadas en la categorías MISC, que se mantienen invariables; el éxito en la identificación de roles de las personas es muy reducido, ya que no alcanza el 50% del total (2/5) y se observan errores importantes.
Es posible que algunos de estos errores se puedan corregir afinando las reglas RegEx a aplicar o implementando procedimientos complementarios, y de hecho ese es el camino que aun podríamos recorrer de seguir dentro de este modelo; pero incluso estas posibilidades acentuan las limitaciones del modelo heurístico, del que, por resumir, indicamos a continuación, y para terminar, ventajas e inconvenientes.
Como ventajas destacar...
- Determinismo absoluto: A diferencia de los modelos probabilísticos, no sufre de alucinaciones ni variabilidad. Ante el mismo texto, produce exactamente el mismo resultado.
- Eficiencia extrema: Mantiene un consumo de RAM plano e ínfimo (frecuentemente inferior a 150 MB), ejecutándose localmente en milisegundos.
- Mantenimiento ágil y soberano: Si el sistema comete un error, no se requiere un complejo reentrenamiento del modelo; basta con ajustar un patrón o un término en el diccionario local.
Y como limitaciones y restricciones...
- Dependencia absoluta de la homogeneidad formal: El sistema es estructuralmente rígido. Si los profesionales modifican significativamente la forma en que se expresan, la tasa de fallos se dispara bruscamente.
- Fragilidad ante la ambigüedad sintáctica: Al depender de ventanas de contexto fijas (por ejemplo, buscar el rol 7 tokens antes o después del nombre), cualquier digresión larga, inciso extenso o subordinada compleja romperá la asociación semántica, impidiemdo la correcta identificación de la entidad.
- Techo de escalabilidad y fatiga de reglas: Aunque el mantenimiento inicial es ágil, el procedimiento tiene un límite de crecimiento. Si el universo documental se expande con demasiadas excepciones, las reglas e inclusiones terminan colisionando entre sí, generando falsos positivos cruzados que requieren un esfuerzo de depuración humana difícil de mantener a la larga.
Lo cierto, no obstante, es que en un proyecto real y de interés para el SEO, aun tendríamos un buen margen de mejora; pero es mejor aun saber que disponemos de otras alternativas, aunque también presenten sus propias limitaciones.
1 Para la elaboración de esta entrada se han consultado diversas fuentes, incluyendo consultaestructurada a Gemini.
2Véase, por ejemplo, el DocAp simple basado en texto mutilado que se puede consultar en esta entrada.
3El EntityRuler es un componente de la popular biblioteca de PLN en Python, spaCy permite identificar, clasificar y extraer información textual basándose estrictamente en un conjunto de instrucciones, reglas o diccionarios predefinidos por el usuario, en lugar de depender de la inferencia estadística de un modelo. Su función es la extracción de entidades nombradas (NER) basada en reglas (Rule-based Named Entity Recognition). Mientras que un modelo de aprendizaje automático adivina qué es una entidad en el texto, el EntityRuler busca coincidencias exactas y etiquetas personalizadas que tú le has indicado previamente.Puedes utilizarlo de las siguientes maneras: (a) Creación de entidades personalizadas: Te permite definir tus propios patrones y asignarles etiquetas (como PRODUCT, PERSON, ORG, etc.) y (b) Combinación con modelos estadísticos: Se puede añadir al pipeline de un modelo existente (como en_core_web_sm) para mejorar la precisión y corregir clasificaciones erróneas.
4 Regex, Regular Expressions o expresiones regulares, es una secuencia de caracteres que forma un patrón de búsqueda que se utiliza en programación, en análisis de datos y en editores de texto para buscar, validar, extraer o reemplazar información específica dentro de grandes bloques de texto de manera rápida y flexible.
5Luego no es un script funcional, como no lo van a ser los que le sigan hasta que se plantee el definitivo, que se indicará específicamente.
6 Este "error" se mantiene conscientemente por motivos didácticos, precisamente para ilustrar las limitaciones de los patrones y la necesidad de especificarlos de forma que se ajusten a la realidad sobre la que se supone que deben actuar.