Skip to content

feat(medcat): Allow bulk superivsed training - #598

Open
mart-r wants to merge 39 commits into
mainfrom
feat/medcat/allow-bulk-superivsed-training
Open

feat(medcat): Allow bulk superivsed training#598
mart-r wants to merge 39 commits into
mainfrom
feat/medcat/allow-bulk-superivsed-training

Conversation

@mart-r

@mart-r mart-r commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

This PR allows bulk supervised training.

The idea is that instead of the previous supervised training method:

def train(cui: str, entity: MutableEntity, doc: MutableDocument, negative: bool, names: dict) -> None

we now use a new one:

def train_supervised_batch(self, examples: list[TrainingExample]) -> None

This refers to the method on the component wishing to be trained in a supervised manner.

The changes

There's a fair few changes that had to be made to make this all work

New protocol for supervised training

The new protocol is the BatchTrainableComponent.
This only defines the new method.
Any future implementation is expected to use this going forward.

The method uses TrainingExample objects.
This is a dataclass that keeps track of information relevant to each example.
This includes stuff like the CUI, the entity, the document, whether it's a negative example, how many epochs to do.
But on top of that, it also keeps track of things (empty for now, but these were also empty before) for name description, status, and ontology (which can be used when adding new names).

Supporting both the old and new method

Because this would otherwise be a breaking change, I wanted to support both the old and the new method.
This means that components that follow the old protocol should still work.
The main workhorse for this is the _LegacyBatchAdapter in type_utils.
This adapts the old style to the new one by calling the old method in sequence.

Avoiding changing names for supervised training

There's a part of the vector context model that allows the (artificial) changing of the name of the concept being trained.
At some chance (as per config), the name that actually appears in the text can be replaced with a synonym.
This was meant to include less-common synonyms in self-supervised training.
However, for supervised training, this does not make a lot of sense.
As such, this was disabled.

I would say that this was somewhat of a bug before.
I don't think it should have ever affected supervised training.

Fixed some annotation/entity mismatch issue

Since the PR #374 (in v2.7.0) there has been an issue with annotation / antity mismatch when doing supervised training.
The culprit was the preprocessing of each annotation that meant that some (ones that weren't able to be mapped to an entity - i.e sub-token annotations) were dropped.
And when iterating over the entities and annotations later (in a zip), after the dropped annotation there will have been mismatches there.

This has now been fixed.

Some changes to string ordering

Due to the random nature of string hashing, getting ordered output from sets (e.g when taking a random sample for name changes) is unpredictable even if the underlying random number generator is seeded.
This can be mitigated by setting the PYTHONHASHSEED environmental variable.
But I didn't think the current situation was desirable - if someone seeds the RNG, they expect to get the same output run to run.

So to mitigate this, in some places (only in the name-change part I believe), string sets are sorted before picking a random item from them.

Some extra debug output

While trying to figure out the differences, I added some extra debug output.
Some of this can be removed if we decide to.
But it helped me identify the various issues I faced along the way.

The testing I did to make sure this still works as close to the previous version as possible:

In order to verify that the updated training method did (roughly speaking) the same thing, I ran a number of tests locally.
I ran the tests for two full models (the 2023 model and the 2025 model).
I ran it over a small number of documents from the entity linking challenge.
I ran it over a number of different configurations:

  • With devalue_others=True/False
    • This was affected by the changes so was keen to do the test with and without
  • With train_from_false_positives=True/False
    • Same as above
  • With full models and traininig-cleared models

During the tests I seeded the PYTHONHASHSEED to avoid string-hashing issues.

As the output I used:

  • Basic info (CDB.get_basic_info)
  • Per CUI count train (CDB.get_cui2count_train)
  • Per name count train (CDB.get_name2count_train)
  • Per CUI context vectors

What I observed was that in all cases the basic information (i.e the first 4 in the above list) were identical.
While the context vectors did change in some scenarios (more on the reasons below), the differences were not huge.

The overall caveats:

There were some differences between the two models were observed in the cases where devalue_others=True was set.
The reason for these differences is boils down to the order of operations.
Previously, the devaluation of others (i.e training on negative examples of other concepts that share the same name to push these further from the current context) was done on a per annotation basis directly after a specific annotation was trained.
However, with the bulk training setup, they are done in bulk after the entire batch has been trained.
This can cause issues due to randomness being involved (e.g for negative sampling), but also due to simply training the same concept / name in a different order (i.e if it had a positive annotation and was considered a negative example due to devalue_others in another annotation in the same document).

Overall I am relatively happy with the small caveat.
But it is still important to note.

mart-r added 30 commits July 22, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant