Manual Span annotations seemingly disappearing when converting to spacy

Hi all,

For an experiment, we manually annotated - via prodigy (v.1.11.11) - a corpus with named entities (using span categorization) and retrieved the annotated dataset using the prodigy recipe data-to-spacy. Because we wanted to do some manual verifications, we wrote a script to compare the manual annotations extracted from prodigy, and the annotations of a model we tested. But while doing this A/B comparison, we then realized that some of the manually annotated entities had "disappeared" in the display of the manually annotated corpus. To be sure, we focused on the manually annotated corpus we had in a .spacy format (spacy v.3.5), and we converted the annotations back into a .json format file in order to see which annotations were retrieved and which were not. See code below:

#Code to create a json file

def visualize(spankey,docbin_file,folder):
    nlp = French()   
    doc_bin = DocBin().from_disk(docbin_file)
    docs = doc_bin.get_docs(nlp.vocab) 
    for i,j in enumerate(docs):
      json_dict = j.to_json(lines=True)
      output_file = open(folder + f"doc{i}.json", "w", encoding="utf-8")
      json.dump(json_dict,output_file)
      output_file.close( )

In the following jsonl file, for example, we observed that only the first entities were annotated and from "44 totems équipés" there is no more entity annotated.

{
    "text": " Les contrôles d’accès sont pour le moment de marque Urbaco et peuvent évolués au cours de celui-ci.\nLe titulaire devra fournir, à la fin de la première année d’exécution, une visite qualifiée « d’audit préalable/état des lieux » \n\nD'autres interventions de modification de l’intérieur d’un équipement sur des appareils de types similaires pourront faire l'objet de bons de commande s'ils venaient à être installés (dans le cadre d’un autre marché d’acquisition de bornes) ou s'ils devaient être incorporés dans le présent accord-cadre au cours de sa période de validité.\n\nEn conséquence, il ne pourra se prévaloir, en aucune circonstance, du manque ou de l'inexistence des spécifications afférentes aux matériels et/ou installations ou d'une méconnaissance des limites de sa prestation.\n\nIl met en œuvre les moyens nécessaires pour compléter et contrôler sur site les éléments lui manquant.\n2.1.5.1 dESCRIPTION SOMMAIRE et nombre DES INSTALLATIONS \n2.1.5.1.1 Bornes escamotables automatiques\nLes bornes escamotables pneumatiques de marque « Urbaco » sont actuellement au nombre de 58. \nElles sont gérées par 44 totems équipés d’interphones, de lecteurs de badges de proximités et pour certaines de LAPI. \n\nLes systèmes de limitation d’accès comprennent des bornes escamotables automatiques gérées par armoire de commande, composés:\n\n· de bornes escamotables pilotées par des vérins pneumatiques\n\n· d’armoire de commande pneumatique avec automate ou PC industriel et système de sécurité.\n\n· de lecteur de badge électronique sans contact\n\n· d’un système de communication par interphonie (RTC et IP) \n\n· d’une gestion centralisée URBACO Sygma III\n\n· Liaison IP pour les sites centralisés\n\n· Système de lecture automatisé de plaques d’immatriculation (LAPI)\n\n2.1.5.1.2 Bornes escamotables semi-automatiques\nLes bornes escamotables semi-automatiques de marque « Urbaco » sont actuellement au nombre de 36. \nLes systèmes de limitation d’accès comprennent des Bornes escamotables semi-automatiques commandées mécaniquement par clé « quart de tour », composés de bornes en acier ou inox pilotées par des vérins à gaz.\n\n2.1.5.1.3 Barrières automatiques\nLa commune possède trois parkings en gestion abonnés composé de 3 barrières d’entrées, 3 barrières de sorties de marques BCA, COMUNELLO et de 10 bornes de commandes équipées de lecteur de badges sans contact URBACO et d’un système d’interphonie IP Castel. \nCes équipements sont contrôlés par des automates industriels.",
    "meta": {
      "title": "../extracted_text/file/localhost/dataset/dataset_cedric/22f192%20cctp%20maintenance%20bornes%20acces.doc_6",
      "size": "60.329 kB"
    },
    "spans": {"sc": [{"start": 5, "end": 22, "label": "PROD", "kb_id": ""}, {"start": 104, "end": 113, "label": "ROLE", "kb_id": ""}, {"start": 153, "end": 158, "label": "TIME", "kb_id": ""}, {"start": 291, "end": 301, "label": "PROD", "kb_id": ""}, {"start": 310, "end": 328, "label": "PROD", "kb_id": ""}, {"start": 465, "end": 471, "label": "PROD", "kb_id": ""}, {"start": 551, "end": 558, "label": "TIME", "kb_id": ""}, {"start": 704, "end": 713, "label": "PROD", "kb_id": ""}, {"start": 720, "end": 733, "label": "PROD", "kb_id": ""}, {"start": 935, "end": 948, "label": "PROD", "kb_id": ""}, {"start": 960, "end": 979, "label": "PROD", "kb_id": ""}, {"start": 997, "end": 1016, "label": "PROD", "kb_id": ""}, {"start": 1109, "end": 1126, "label": "PROD", "kb_id": ""}]}
}

Do you have any idea why these annotations "disappear"?
Thanks !

Amandine

hi @Amandine_Lbs!

Thanks for your question and welcome to the Prodigy community :wave:

Sorry to hear about the issue.

I'm scratching my head because nothing sounds obvious but I wonder if we can do some checks to account for the annotations at each step.

Just curious - can you run db-out first and inspect that output .jsonl?

prodigy db-out my_dataset > my_dataset.jsonl

Then you can run basic stats:

import srsly

examples = srsly.read_jsonl("my_dataset.jsonl")

span_cnt = 0
for eg in examples:
    for span in eg.get("spans"):
        span_cnt += 1

print(span_cnt)
print(len(examples))

Alternatively, you can try to pull all of the examples directly from your database using:

from prodigy.components.db import connect

db = connect() 
examples = db.get_dataset("your_dataset")

span_cnt = 0
for eg in examples:
    for span in eg.get("spans"):
        span_cnt += 1

print(span_cnt)
print(len(examples))

I think this is important first to diagnose any missing spans/records before running data-to-spacy or running custom scripts.

If the records are all there, at least you know it was saved in the database. Then you can be more confident there is a loss somewhere along the process of conversion.

If you don't know the exact count, perhaps you can run assert statements in the loop, looking specifically for 1 or 2 examples that you're confident should be in your data like "44 totems équipés".

For data-to-spacy, you can run spacy debug data on your spacy binary files / config to get some stats:

spacy debug data config.cfg --paths.train train.spacy --paths.dev dev.spacy

It can provide counts and number of spans by entity type. This would be helpful to provide too compared to your earlier numbers so we can account for them.

For the conversion back to .jsonl from binary, I don't see any major problems in your code (I did notice you have spankey as an argument but it isn't used). Just to make sure, you can also try this snippet to convert the binary files to .jsonl:

So if you can check for these:

  • Prodigy database counts: db-out and/or DB components to check for spans
  • spacy debug data: check for spaCy binary files from data-to-spacy
  • Run alternative binary to .jsonl conversion along with your script

Hopefully, you'll find out if there's any loss between any step. Let me know what you find!