5.3. Código#

Note

El proyecto completo está disponible en el repositorio MedDocAn de nuestro github donde todo el código así como los experimentos del proyecto son disponibles.

5.3.1. Meddocan pipeline#

Una de las tareas es obtener un preprocesso correcto de los informes clínicos a través de un objeto spacy.tokens.Doc a partir de cualquier cadena de caracteres. Por ello utilizaremos el meddocan.language.pipeline.meddocan_pipeline creado con la ayuda de la biblioteca spaCy para adaptarlo a nuestras necesidades. El código relativo a la creación del pipeline se puede encontrar en el paquete meddocan/language.

Por otra parte, tenemos que poder leer y extraer la información relevante de los documentos provisto en el formato Brat. Es decir que cada informe clínico esta compuesto de 2 ficheros. Uno contiene el texto bruto y el otro las anotaciones. El código relativo de esa parte se encuentra en el paquete meddocan/data.

Para ver cómo funciona, seleccionamos un informe médico gracias al objeto meddocan.data.docs_iterators.GsDocs que permite acceder a los documentos del conjunto de datos meddocan directamente como objetos spacy.tokens.Doc con varios atributos específicos.

from pathlib import Path
from typing import List, Tuple

import pandas as pd
from spacy import displacy

from meddocan.data import meddocan_zip, ArchiveFolder
from meddocan.data.containers import BratAnnotations, BratFilesPair, BratSpan
from meddocan.data.docs_iterators import GsDocs

gs_docs = GsDocs(ArchiveFolder.train)
docs_with_brat_pair = iter(gs_docs)
doc_with_brat_pair = next(docs_with_brat_pair)

El objeto doc_with_brat_pair creado por GsDocs tiene 2 atributos.

[attr for attr in vars(doc_with_brat_pair).keys()]
['brat_files_pair', 'doc']

El atributo brat_files_pair es un objeto meddocan.data.docs_iterators.BratFilesPair que indica la ubicación de los ficheros originales correspondiendo al atributo doc.

pd.DataFrame(
    [type(doc_with_brat_pair.brat_files_pair).__qualname__,
     doc_with_brat_pair.brat_files_pair.ann.name,
     doc_with_brat_pair.brat_files_pair.txt.name],
    index=["type", "txt", "ann"]
).T
type txt ann
0 BratFilesPair S0004-06142005000500011-1.ann S0004-06142005000500011-1.txt

Lo que hace GsDocs es crear un objeto Doc a partir de un objeto meddocan.data.docs_iterators.DocWithBratPair utilizando el MedocanPipeline.

En una primera fase el MeddocanPipeline recibe el texto contenido en el fichero original S0004-06142005000500011-1.txt como argumento.

Miramos el contenido del fichero utilizando el atributo doc.

gold = doc_with_brat_pair.doc
gold
Datos del paciente.
Nombre:  Ernesto.
Apellidos: Rivera Bueno.
NHC: 368503.
NASS: 26 63514095.
Domicilio:  Calle Miguel Benitez 90.
Localidad/ Provincia: Madrid.
CP: 28016.
Datos asistenciales.
Fecha de nacimiento: 03/03/1946.
País: España.
Edad: 70 años Sexo: H.
Fecha de Ingreso: 12/12/2016.
Médico:  Ignacio Navarro Cuéllar NºCol: 28 28 70973.
Informe clínico del paciente: Paciente de 70 años de edad, minero jubilado, sin alergias medicamentosas conocidas, que presenta como antecedentes personales: accidente laboral antiguo con fracturas vertebrales y costales; intervenido de enfermedad de Dupuytren en mano derecha y by-pass iliofemoral izquierdo; Diabetes Mellitus tipo II, hipercolesterolemia e hiperuricemia; enolismo activo, fumador de 20 cigarrillos / día.
Es derivado desde Atención Primaria por presentar hematuria macroscópica postmiccional en una ocasión y microhematuria persistente posteriormente, con micciones normales.
En la exploración física presenta un buen estado general, con abdomen y genitales normales; tacto rectal compatible con adenoma de próstata grado I/IV.
En la analítica de orina destaca la existencia de 4 hematíes/ campo y 0-5 leucocitos/campo; resto de sedimento normal.
Hemograma normal; en la bioquímica destaca una glucemia de 169 mg/dl y triglicéridos de 456 mg/dl; función hepática y renal normal. PSA de 1.16 ng/ml.
Las citologías de orina son repetidamente sospechosas de malignidad.
En la placa simple de abdomen se valoran cambios degenerativos en columna lumbar y calcificaciones vasculares en ambos hipocondrios y en pelvis.
La ecografía urológica pone de manifiesto la existencia de quistes corticales simples en riñón derecho, vejiga sin alteraciones con buena capacidad y próstata con un peso de 30 g.
En la UIV se observa normofuncionalismo renal bilateral, calcificaciones sobre silueta renal derecha y uréteres arrosariados con imágenes de adición en el tercio superior de ambos uréteres, en relación a pseudodiverticulosis ureteral. El cistograma demuestra una vejiga con buena capacidad, pero paredes trabeculadas en relación a vejiga de esfuerzo. La TC abdominal es normal.
La cistoscopia descubre la existencia de pequeñas tumoraciones vesicales, realizándose resección transuretral con el resultado anatomopatológico de carcinoma urotelial superficial de vejiga.
Remitido por: Ignacio Navarro Cuéllar c/ del Abedul 5-7, 2º dcha 28036 Madrid, España E-mail: nnavcu@hotmail.com.

En una segunda fase se le asigna las entidades extraídas del fichero S0004-06142005000500011-1.ann utilizando el método spacy.tokens.Doc.set_ents.
Miramos el fichero al formato .brat que contiene las anotaciones para hacerse una idea.

print(doc_with_brat_pair.brat_files_pair.ann.read_text())
T1	FECHAS 215 225	03/03/1946
T2	CORREO_ELECTRONICO 2421 2439	nnavcu@hotmail.com
T3	PAIS 2406 2412	España
T4	TERRITORIO 2398 2404	Madrid
T5	TERRITORIO 2392 2397	28036
T6	CALLE 2365 2391	c/ del Abedul 5-7, 2º dcha
T7	NOMBRE_PERSONAL_SANITARIO 303 326	Ignacio Navarro Cuéllar
T8	NOMBRE_PERSONAL_SANITARIO 2341 2364	Ignacio Navarro Cuéllar
T9	EDAD_SUJETO_ASISTENCIA 389 396	70 años
T10	ID_TITULACION_PERSONAL_SANITARIO 334 345	28 28 70973
T11	FECHAS 282 292	12/12/2016
T12	SEXO_SUJETO_ASISTENCIA 261 262	H
T13	EDAD_SUJETO_ASISTENCIA 247 254	70 años
T14	PAIS 233 239	España
T15	TERRITORIO 166 171	28016
T16	TERRITORIO 154 160	Madrid
T17	CALLE 107 130	Calle Miguel Benitez 90
T18	ID_ASEGURAMIENTO 82 93	26 63514095
T19	ID_SUJETO_ASISTENCIA 68 74	368503
T20	NOMBRE_SUJETO_ASISTENCIA 49 61	Rivera Bueno
T21	NOMBRE_SUJETO_ASISTENCIA 29 36	Ernesto

Ahora que tenemos una idea más clara de nuestros datos originales, observamos la serie de pre-procesos aplicados por el meddocan_pipeline y GsDocs gracias a la instancia gold del objeto Doc para preparar el conjunto de datos a fin de entrenar una red neuronal con Flair.

Note

En el ejemplo solo miramos las 3 primeras lineas del objeto Doc.

max_lines = 3

for i, sent in enumerate(gold.sents):
    print(f"--------------- Sentence {i + 1} ---------------")
    a = zip(*((tok.text, tok.ent_iob_, tok.ent_type_) for tok in sent))
    df = pd.DataFrame(a, index=["text", "bio", "etiqueta"])
    display(df.T)
    if i >= max_lines - 1:
        break
--------------- Sentence 1 ---------------
text bio etiqueta
0 Datos O
1 del O
2 paciente O
3 . O
4 \n O
--------------- Sentence 2 ---------------
text bio etiqueta
0 Nombre O
1 : O
2 O
3 Ernesto B NOMBRE_SUJETO_ASISTENCIA
4 . O
5 \n O
--------------- Sentence 3 ---------------
text bio etiqueta
0 Apellidos O
1 : O
2 Rivera B NOMBRE_SUJETO_ASISTENCIA
3 Bueno I NOMBRE_SUJETO_ASISTENCIA
4 . O
5 \n O

Para entender un poco mejor lo que hacemos miramos los diferentes componentes del MeddocanPipeline.

pd.DataFrame(gs_docs.nlp.pipe_names, columns=["componentes"]).T
0 1 2 3
componentes missaligned_splitter line_sentencizer predictor write_methods
  1. El primer elemento de nuestro pipeline es el tokenizer seguido del componente missaligned_splitter que nos permite afinar la tokenización de tal forma que cada token se corresponda exactamente con una etiqueta al formato BIO.

  2. El segundo componente, line_sentencizer permite partir el texto en frases. En este caso se corresponde a un párrafo.

  3. El componente predictor nos permite utilizar un modelo de Flair de tal forma que se integra al pipeline. De esa mañera se puede hacer directamente predicciones utilizando un objeto Doc y un modelo entrenado previamente.

  4. El componente write_methods añade los métodos to_connl03 y to_ann al objeto Doc que sirven a crear los ficheros necesarios para:

    • Crear un flair.data.Corpus que va a permitir entrenar un modelo con Flair.

    • Evaluar un modelo utilizando el script de evaluación propio de la competición.

Note

Hemos integrado el script de evaluación dentro de nuestra librería con algunas modificaciones y un poco más de documentación, con el objetivo de unificar el workflow del entrenamiento hasta la evaluación.
La evaluación se hace entonces directamente desde nuestra librería gracias al commando:

$ meddocan eval --help
Usage: meddocan eval [OPTIONS] MODEL NAME
Evaluate the model with the `meddocan` metrics.
    
    Compute f1-score for Ner (start, end, tag), Span (start, end) and merged
    span if not there is no number or letter between consecutive span.

    The function produce the following temporary folder hierarchy:

    evaluation_root
    ├── golds
    │   ├── dev
    |   |    └── brat
    |   |       ├── file-1.ann
    |   |       ├── file-1.txt
    |   |       ├── ...
    |   |       └── file-n.ann
    |   └── test
    |        └── brat
    |           ├── file-1.ann
    |           ├── file-1.txt
    |           ├── ...
    |           └── file-n.ann

    └── name
        ├── dev
        |    └── brat
        |       ├── file-1.ann
        |       ├── file-1.txt
        |       ├── ...
        |       └── file-n.ann
        └── test
             └── brat
                ├── file-1.ann
                ├── file-1.txt
                ├── ...
                └── file-n.ann

    Then the model is evaluate producing the following files:

    evaluation_root/name
    ├── dev
    │   ├── ner
    │   └── spans
    └── test
        ├── ner
        └── spans

    And the temporary folder are removed.

    Args:
        model (str): Path to the Flair model to evaluate.
        name (str): Name of the folder that will holds the results produced by\
            the Flair model.
        evaluation_root (str): Path to the root folder where the
            results will be stored.
        sentence_splitting (Path): Path to the sub-directory
            `sentence_splitting`. This directory is mandatory to compute the
            `leak score` evaluation metric.
        force (bool, optional): Force to create again the golds standard files.
            Defaults to False.

Arguments:
  MODEL  Path to the Flair model to evaluate.  [required]
  NAME   Name of the folder that will holds the results produced by the
         Flair model.  [required]

Options:
  --evaluation-root PATH     Path to the root folder where the results will be
                             stored.
  --sentence-splitting PATH  The sub-directory `sentence_splitting` is
                             mandatory to compute the `leak score` evaluation
                             metric.
  --device TEXT              Device to use.  [default: cuda:0]
  --help                     Show this message and exit.        

La entidades anotadas se obtienen utilizando el atributo ents de nuestro objeto spacy.tokens.Doc.

pd.DataFrame(
    zip(*[(ent.text, ent.start_char, ent.end_char) for ent in gold.ents]),
    index=["Tag", "start", "end"]
).T
Tag start end
0 Ernesto 29 36
1 Rivera Bueno 49 61
2 368503 68 74
3 26 63514095 82 93
4 Calle Miguel Benitez 90 107 130
5 Madrid 154 160
6 28016 166 171
7 03/03/1946 215 225
8 España 233 239
9 70 años 247 254
10 H 261 262
11 12/12/2016 282 292
12 Ignacio Navarro Cuéllar 303 326
13 28 28 70973 334 345
14 70 años 389 396
15 Ignacio Navarro Cuéllar 2341 2364
16 c/ del Abedul 5-7, 2º dcha 2365 2391
17 28036 2392 2397
18 Madrid 2398 2404
19 España 2406 2412
20 nnavcu@hotmail.com 2421 2439

Nuestras entidades son en este ejemplo compuestas del tag y del a position de la entidad en la cadena de caracteres original.

Ahora si queremos algo más visual podemos utilizar la function spacy.displacy.render() que nos permite trabajar con el objeto Doc.

displacy.render(gold, style="ent")
Datos del paciente.
Nombre: Ernesto NOMBRE_SUJETO_ASISTENCIA .
Apellidos: Rivera Bueno NOMBRE_SUJETO_ASISTENCIA .
NHC: 368503 ID_SUJETO_ASISTENCIA .
NASS: 26 63514095 ID_ASEGURAMIENTO .
Domicilio: Calle Miguel Benitez 90 CALLE .
Localidad/ Provincia: Madrid TERRITORIO .
CP: 28016 TERRITORIO .
Datos asistenciales.
Fecha de nacimiento: 03/03/1946 FECHAS .
País: España PAIS .
Edad: 70 años EDAD_SUJETO_ASISTENCIA Sexo: H SEXO_SUJETO_ASISTENCIA .
Fecha de Ingreso: 12/12/2016 FECHAS .
Médico: Ignacio Navarro Cuéllar NOMBRE_PERSONAL_SANITARIO NºCol: 28 28 70973 ID_TITULACION_PERSONAL_SANITARIO .
Informe clínico del paciente: Paciente de 70 años EDAD_SUJETO_ASISTENCIA de edad, minero jubilado, sin alergias medicamentosas conocidas, que presenta como antecedentes personales: accidente laboral antiguo con fracturas vertebrales y costales; intervenido de enfermedad de Dupuytren en mano derecha y by-pass iliofemoral izquierdo; Diabetes Mellitus tipo II, hipercolesterolemia e hiperuricemia; enolismo activo, fumador de 20 cigarrillos / día.
Es derivado desde Atención Primaria por presentar hematuria macroscópica postmiccional en una ocasión y microhematuria persistente posteriormente, con micciones normales.
En la exploración física presenta un buen estado general, con abdomen y genitales normales; tacto rectal compatible con adenoma de próstata grado I/IV.
En la analítica de orina destaca la existencia de 4 hematíes/ campo y 0-5 leucocitos/campo; resto de sedimento normal.
Hemograma normal; en la bioquímica destaca una glucemia de 169 mg/dl y triglicéridos de 456 mg/dl; función hepática y renal normal. PSA de 1.16 ng/ml.
Las citologías de orina son repetidamente sospechosas de malignidad.
En la placa simple de abdomen se valoran cambios degenerativos en columna lumbar y calcificaciones vasculares en ambos hipocondrios y en pelvis.
La ecografía urológica pone de manifiesto la existencia de quistes corticales simples en riñón derecho, vejiga sin alteraciones con buena capacidad y próstata con un peso de 30 g.
En la UIV se observa normofuncionalismo renal bilateral, calcificaciones sobre silueta renal derecha y uréteres arrosariados con imágenes de adición en el tercio superior de ambos uréteres, en relación a pseudodiverticulosis ureteral. El cistograma demuestra una vejiga con buena capacidad, pero paredes trabeculadas en relación a vejiga de esfuerzo. La TC abdominal es normal.
La cistoscopia descubre la existencia de pequeñas tumoraciones vesicales, realizándose resección transuretral con el resultado anatomopatológico de carcinoma urotelial superficial de vejiga.
Remitido por: Ignacio Navarro Cuéllar NOMBRE_PERSONAL_SANITARIO c/ del Abedul 5-7, 2º dcha CALLE 28036 TERRITORIO Madrid TERRITORIO , España PAIS E-mail: nnavcu@hotmail.com CORREO_ELECTRONICO .

5.3.2. Entrenar un modelo con Flair#

La clase GsDocs sirve a preparar los datos de forma que puedan ser leídos por Flair. Esto lo hacemos a través del objeto meddocan.data.corpus.MEDDOCAN que hereda de flair.datasets.ColumnCorpus.

Este corpus permite a la biblioteca Flair acceder directamente a los conjuntos de datos de entrenamiento, validación y prueba.

from meddocan.data.corpus import MEDDOCAN

corpus = MEDDOCAN(sentences=True, in_memory=True, document_separator_token="-DOCSTART-")
2022-11-08 11:12:52,616 Reading data from /tmp/tmp7uu7l1fq
2022-11-08 11:12:52,617 Train: /tmp/tmp7uu7l1fq/train
2022-11-08 11:12:52,618 Dev: /tmp/tmp7uu7l1fq/dev
2022-11-08 11:12:52,618 Test: /tmp/tmp7uu7l1fq/test

Una vez creado nuestro corpus podemos utilizar los métodos de los que hereda nuestro objeto como el método especial __str__ que escribe en stdout el número de objetos flair.tokens.Sentence que contiene cada subconjunto del conjunto de datos. Es decir, cuántos párrafos tenemos en total en cada uno de nuestros conjuntos de datos.

print(corpus)
Corpus: 10811 train + 5518 dev + 5405 test sentences

Para entrenar los modelos con la librería Flair basta seguir el ejemplo siguiente.

Note

Todos nuestros experimentos se pueden encontrar en la carpeta experiments y siguen este formato.
La única diferencia es la adición de las librerías hyperopt 1 y Tensorboard 2 para obtener una trazabilidad de los entrenamientos mediante el registro de diversos parámetros y resultados.

from flair.data import Corpus
from flair.embeddings import TransformerWordEmbeddings
from flair.models import SequenceTagger
from flair.trainers import ModelTrainer

from meddocan.data.corpus import MEDDOCAN

# 1. Obtener el corpus
corpus: Corpus = MEDDOCAN(
    sentences=True, document_separator_token="-DOCSTART-"
)
print(corpus)

# 2. Que label se quiere predecir?
label_type = 'ner'

# 3. Crear el diccionario de labels a partir del corpus
label_dict = corpus.make_label_dictionary(label_type=label_type, add_unk=False)
print(label_dict)

# 4. Inicializar los embeddings generados por el transformador utilizando el contexto
embeddings = TransformerWordEmbeddings(model='dccuchile/bert-base-spanish-wwm-cased',
                                       layers="-1",
                                       subtoken_pooling="first",
                                       fine_tune=True,
                                       use_context=True,
                                       )

# 5. Inicializar etiquedator simple (no CRF, no RNN, no reprojección)
tagger = SequenceTagger(hidden_size=256,
                        embeddings=embeddings,
                        tag_dictionary=label_dict,
                        tag_type='ner',
                        use_crf=False,
                        use_rnn=False,
                        reproject_embeddings=False,
                        )

# 6. Initializar el trainer
trainer = ModelTrainer(tagger, corpus)

# 7. Ejecutar el fine-tuning
trainer.fine_tune('experiments/meddocan',
                  learning_rate=5.0e-6,
                  mini_batch_size=4,
                  )

5.3.3. Inferencia#

Una vez el modelo entrenado, la inferencia se hace gracias al meddocan_pipeline justamente porque nos permite integrar un modelo de Flair gracias a su componente predictor como lo vamos a ver a continuación.

Note

Aquí utilizamos por el ejemplo un modelo de FLair pre-entrenado con los embeddings de Flair y una red LSTM-CRF y entrenado sobre el conjunto de datos CONLL-2002 en inglés. Este conjunto de datos consiste en artículos de noticias anotados con las categorías LOC, PER y ORG que son diferentes de las categorías de MEDDOCAN.

from meddocan.language.pipeline import meddocan_pipeline

nlp = meddocan_pipeline("flair/ner-english-fast")
sys = nlp(gold.text)
2022-11-08 11:13:00,076 loading file /home/wave/.meddocan/models/ner-english-fast/4c58e7191ff952c030b82db25b3694b58800b0e722ff15427f527e1631ed6142.e13c7c4664ffe2bbfa8f1f5375bd0dced866b8c1dd7ff89a6d705518abf0a611
2022-11-08 11:13:02,946 SequenceTagger predicts: Dictionary with 20 tags: <unk>, O, S-ORG, S-MISC, B-PER, E-PER, S-LOC, B-ORG, E-ORG, I-PER, S-PER, B-MISC, I-MISC, E-MISC, I-ORG, B-LOC, E-LOC, I-LOC, <START>, <STOP>

El objeto sys es un objeto spacy.tokens.Doc al igual de el objeto gold.

La única diferencia entre sys y gold es que sys contiene entidades que le son asignadas por un modelo entrenado con los algoritmos de Flair, mientras que en el caso de gold provienen de la lectura de un archivo .ann.

Entonces para visualizar las predicciones de nuestro model, lo tenemos igual de fácil que antes. Basta utilizar la function spacy.displacy.render().

displacy.render(sys, style="ent")
Datos del paciente ORG .
Nombre: Ernesto PER .
Apellidos: Rivera Bueno PER .
NHC: 368503.
NASS ORG : 26 63514095.
Domicilio: Calle Miguel Benitez PER 90.
Localidad LOC / Provincia LOC : Madrid LOC .
CP ORG : 28016.
Datos asistenciales.
Fecha de nacimiento MISC : 03/03/1946.
País: España ORG .
Edad ORG : 70 años Sexo: H.
Fecha de Ingreso ORG : 12/12/2016.
Médico ORG : Ignacio Navarro Cuéllar NºCol ORG : 28 28 70973.
Informe clínico del paciente: Paciente de 70 años de edad MISC , minero jubilado, sin alergias medicamentosas conocidas, que presenta como antecedentes personales: accidente laboral antiguo con fracturas vertebrales y costales; intervenido de enfermedad de Dupuytren en mano derecha y by-pass iliofemoral izquierdo; Diabetes Mellitus tipo II, hipercolesterolemia e hiperuricemia; enolismo activo, fumador de 20 cigarrillos / día.
Es derivado desde Atención Primaria por presentar hematuria macroscópica postmiccional en una ocasión y microhematuria persistente posteriormente, con micciones normales.
En la exploración física presenta un buen estado general, con abdomen y genitales normales; tacto rectal compatible con adenoma de próstata grado I/IV.
En la analítica de orina destaca la existencia de 4 hematíes/ campo y 0-5 leucocitos/campo; resto de sedimento normal.
Hemograma normal; en la bioquímica destaca una glucemia de 169 mg/dl y triglicéridos de 456 mg/dl; función hepática y renal normal. PSA de 1.16 ng/ml.
Las citologías de orina son repetidamente sospechosas de malignidad.
En la placa simple de abdomen se valoran cambios degenerativos en columna lumbar y calcificaciones vasculares en ambos hipocondrios y en pelvis.
La ecografía urológica pone de manifiesto la existencia de quistes corticales simples en riñón derecho, vejiga sin alteraciones con buena capacidad y próstata con un peso de 30 g.
En la UIV MISC se observa normofuncionalismo renal bilateral, calcificaciones sobre silueta renal derecha y uréteres arrosariados con imágenes de adición en el tercio superior de ambos uréteres, en relación a pseudodiverticulosis ureteral. El cistograma demuestra una vejiga con buena capacidad, pero paredes trabeculadas en relación a vejiga de esfuerzo. La TC abdominal es normal.
La cistoscopia descubre la existencia de pequeñas tumoraciones vesicales, realizándose resección transuretral con el resultado anatomopatológico de carcinoma urotelial superficial de vejiga.
Remitido por: Ignacio Navarro Cuéllar PER c/ del Abedul PER 5-7, 2º dcha 28036 Madrid LOC , España E ORG -mail: nnavcu@hotmail.com.

Note

Vemos a ojo que el modelo de Flair flair/ner-english-fast parece detectar correctamente el span de ciertas entidades. Les asigna una etiqueta como LOC o PERS y efectivamente son direcciones o personas aunque por supuesto no tiene el mismo etiquetado.

5.3.4. Evaluation#

La evaluación originalmente provistas a través del script de evaluación que re-utilizamos, utiliza el texto original así como su anotación al formato brat para calcular las métricas según las tareas Subtrack1, Subtrack2 [Strict] y SubTrack2 [Merged].

Para evaluar nuestros modelos, utilizamos el texto original a partir del cual se ha creado el documento sys así como su atributo _.to_ann. Ese método permite codificar el atributo ents del objeto sys en un fichero siguiendo el formado brat como se puede ver a continuación.

from tempfile import TemporaryDirectory

with TemporaryDirectory() as td:
    pth = Path(td, "file.txt")
    sys._.to_ann(pth)
    for i, line in enumerate(pth.read_text().split("\n")):
        print(line)
T_0	ORG 0 18	Datos del paciente
T_1	PER 29 36	Ernesto
T_2	PER 49 61	Rivera Bueno
T_3	ORG 76 80	NASS
T_4	PER 107 127	Calle Miguel Benitez
T_5	LOC 132 141	Localidad
T_6	LOC 143 152	Provincia
T_7	LOC 154 160	Madrid
T_8	ORG 162 164	CP
T_9	MISC 194 213	Fecha de nacimiento
T_10	ORG 233 239	España
T_11	ORG 241 245	Edad
T_12	ORG 264 280	Fecha de Ingreso
T_13	ORG 294 300	Médico
T_14	ORG 303 332	Ignacio Navarro Cuéllar NºCol
T_15	MISC 377 404	Paciente de 70 años de edad
T_16	MISC 1758 1767	En la UIV
T_17	PER 2341 2364	Ignacio Navarro Cuéllar
T_18	PER 2368 2378	del Abedul
T_19	LOC 2398 2404	Madrid
T_20	ORG 2406 2414	España E

Para hacer esto más automatizado, al igual que la clase meddocan.data.docs_iterators.GsDocs, tenemos la clase from meddocan.data.docs_iterators.SysDocs que utiliza un modelo de Flair para detectar entidades sobre los documentos de cada sub-conjunto de datos. Veamos un ejemplo:

from meddocan.data.docs_iterators import SysDocs
from meddocan.data import ArchiveFolder

import torch
import flair

flair.device = torch.device("cuda:1")

sys_docs = iter(SysDocs(archive_name=ArchiveFolder.test, model="flair/ner-spanish-large"))
2022-11-08 11:13:05,538 loading file /home/wave/.meddocan/models/ner-spanish-large/045ad6c7dc21e0eb85935dce0544eec65f8c63c58412154df4dee7ff5f11665b.d4d3456316d2951bc100d060bd63a690b33af6d273adffa1b90df32328ed3257
2022-11-08 11:13:34,905 SequenceTagger predicts: Dictionary with 20 tags: <unk>, O, S-LOC, S-ORG, B-PER, I-PER, E-PER, S-MISC, B-ORG, E-ORG, S-PER, I-ORG, B-LOC, E-LOC, B-MISC, E-MISC, I-MISC, I-LOC, <START>, <STOP>

Gracias a las clase GsDocs y SysDocs podemos producir fácilmente los ficheros requeridos para usar el script de evaluación provisto a través de la la linea de comando meddocan eval.

Como curiosidad podemos explicar como se pueden calcular las métricas.

gs_docs = iter(GsDocs(archive_name=ArchiveFolder.test))

Recuperamos los iteradores sys_doc y gold_doc y comprobamos que corresponden a los mismos documentos originales, tanto en su origen como en su contenido.

sys_doc, gold_doc = next(sys_docs), next(gs_docs)

assert sys_doc.brat_files_pair.ann.name == gold_doc.brat_files_pair.ann.name
assert sys_doc.brat_files_pair.txt.name == gold_doc.brat_files_pair.txt.name
assert sys_doc.doc.text == gold_doc.doc.text

Ahora recuperamos las entidades originales en gold_labels y las predicciones sys_labels.

from meddocan.evaluation.classes import Ner, Span

gold_labels = set(Ner(ent.start, ent.end, ent.label_) for ent in gold.ents)
sys_labels = set(Ner(ent.start, ent.end, ent.label_) for ent in sys.ents)

Por fin calculamos el score \(F_{1} micro\) para el documento así como el recall y la precision.

from typing import TypeVar

T = TypeVar("T", Ner, Span)

def f1(gold_label: List[T], sys_label: List[T]) -> float:
    tp = gold_label.intersection(sys_label)
    fp = sys_label - gold_label
    fn = gold_label - sys_label
    try:
        recall = len(tp) / float(len(tp) + len(fp))
    except ZeroDivisionError:
        recall = 0.0
    try:
        precision = len(tp) / float(len(tp) + len(fn))
    except ZeroDivisionError:
        precision = 0.0
    try:
        f1 = 2 * (recall * precision) / (recall + precision)
    except ZeroDivisionError:
        f1 = 0.0
    return f1, recall, precision

pd.DataFrame(
    f1(gold_labels, sys_labels),
    index=["f1", "recall", "precisión"],
    columns=["Subtrack1"]
).T
f1 recall precisión
Subtrack1 0.0 0.0 0.0

¡El score \(F_{1} micro\) esta nulo simplemente porque el modelo utilizado no predice las mismas entidades y ademas esta entrenado sobre un conjunto de datos en inglés! Pero si se trata únicamente de anonimizar y que usamos solo los Span puede que no sea lo mismo dado que ahora no importan las etiquetas sino solo la posición de las entidades.

gold_spans = set(Span(ent.start, ent.end) for ent in gold.ents)
sys_spans = set(Span(ent.start, ent.end) for ent in sys.ents)
pd.DataFrame(
    f1(gold_spans, sys_spans),
    index=["f1", "recall", "precision"],
    columns=["Subtrack2[strict]"]
).T
f1 recall precision
Subtrack2[strict] 0.285714 0.285714 0.285714

De hecho, vemos que incluso con un modelo entrenado en un conjunto de datos muy diferente con tan sólo 4 entidades, conseguimos anonimizar un poco más de un cuartos de los span deseados.


1

https://github.com/hyperopt/hyperopt

2

https://pytorch.org/docs/stable/tensorboard.html