Feat: Proposal config builder system#904
Conversation
…f the concrete builders to initialise a minimum config dict
jdeschamps
left a comment
There was a problem hiding this comment.
I didn't spend as much time as I wanted... But I am just going to write down my thoughts so we can start discussing.
I like the principle, as it may be elegant on the user side. I do worry however that it will be hard to maintain with an explosion of small classes. While they are well-defined, some parameters are broken off from their logical place (e.g. axes are in the base class, not in data), making it hard to follow. Long range interactions (e.g. disabling validation, 3D vs 2D, n_input and channels) are also a bit tricky, since many parameters interact at different levels and it is hard to ensure that they are all compatible when they are set one at a time, so the consequence is going to be complex mixins. Units of parameters working together may end up in different classes.
In addition, I am not sure that it will be intuitive for users. A function with a long list of parameters is annoying, but it is at least clear what parameters can be used. It also allows dealing with the long range interaction much more easily.
The argument in favor is obviously extensibility, which will be easier than the current convenience functions. The long range interactions could be mixins of their own (e.g. channels + n_input_channels), I guess the docs are a good place to see what is typically changed together. Although redefining the same parameter in different mixins sounds like a bad idea. We should examine whether the error raised in the convenience functions could just move to a Pydantic validation.
I did not see any error raised, probably because it is work in progress, I suppose it could also be added to guide users on sets of incompatible parameters.
You say the defaults are all delegated to the Pydantic model, as it should. That is not the case for the training config though.
One idea: we could also instantiate a Builder from an existing configuration.
I am not in favor of including this into v0.2 because I'd like to go ahead with the release, we could introduce it in later patch versions and make it the default in v0.3 (alongside MSplit support) with a deprecation warning for the linear functions.
| @overload | ||
| def set_early_stopping( | ||
| self: ConfigBuilderT, on: Literal[True], **kwargs: Any | ||
| ) -> ConfigBuilderT: ... | ||
|
|
||
| @overload | ||
| def set_early_stopping( | ||
| self: ConfigBuilderT, on: Literal[False] | ||
| ) -> ConfigBuilderT: ... | ||
|
|
||
| def set_early_stopping( | ||
| self: ConfigBuilderT, on: bool, **kwargs: Any | ||
| ) -> ConfigBuilderT: |
There was a problem hiding this comment.
Is the overload really justified?
There was a problem hiding this comment.
If set_early_stopping is not called, then it is off and not in the dict, if it is called then the kwargs are passed.
There was a problem hiding this comment.
Ok, I see it is to be able to disable the early stopping if it was there originally. I'd rather have set_early_stopping(*kwargs) and disable_early_stopping() methods
| # set default checkpointing params (n2n self supervised) | ||
| # (can be overwritten with set_checkpoint_params from TrainingParamMixin) | ||
| self.config_dict["training_config"]["checkpoint_params"] = asdict( | ||
| SelfSupervisedCheckpointing() | ||
| ) | ||
|
|
||
| # no early stopping by default | ||
| self.config_dict["training_config"]["early_stopping_params"] = None |
There was a problem hiding this comment.
Wouldn't it be better to not add a training_config, only adding it if the specific methods are called, and leave it to the default constructor to set the defaults?
| early_stopping_params: NotRequired[dict[str, Any] | None] | ||
|
|
||
|
|
||
| class ConfigDict(TypedDict): |
There was a problem hiding this comment.
Confusing naming because of Pydantic's ConfigDict.
Yes the reason why I did not put axes into the I agree that the long range interaction between different levels of the config is a bit tricky, and maybe annoying to have to call multiple methods to set the configuration correctly.
Yeah I think I would like to see some user testings to see if it is intuitive or not. In vscode, typing the
Yes I was torn between how to group parameters in a way that made sense. While I also was originally thinking there should not be any overlap between parameters set by different methods, now I am considering maybe it would be ok, if you change the same parameter in two different methods then it will be the last method called that will overwrite it last.
I identified that some of the errors raise in the convenience functions duplicate validation that is now present in the Configuration classes with the introduction of careamics/src/careamics/config/factories/n2v_factory.py Lines 379 to 382 in 0b5b1f0 careamics/src/careamics/config/factories/n2v_factory.py Lines 400 to 404 in 0b5b1f0
Do you mean
I agree, it definitely needs more discussion and refining |
|
Alright, so plan of action could be:
The first one not really since it corresponds to the number of input in the model, and there is no way to tell apart the default value and what the user set (as opposed to a function with a default equal to |
Disclaimer
Description
Note
tldr: Draft PR as a proposal for a config builder system that will be hopefully easier to maintain (and use) as we add new algorithms and features. No docs or tests yet this is just to discuss whether we like the idea or not, and the implementation if we do like it.
Background - why do we need this PR?
I wanted to make the code for setting certain groups of parameters in the config more easily reusable and modifiable between algorithms.
Overview - what changed?
Introduced builder classes for each algorithm
N2VConfigBuilder,CareConfigBuilderandN2NConfigBuilderthat can be used to replace thecreate_n2v_config,create_advanced_n2v_configetc.Hopefully the mechanism will allow introducing new algorithm config builders, e.g.
MicrosplitConfigBuilderetc., more easily.Implementation - how did you implement the changes?
Builder classes
The builder classes can be instantiated with a basic set of parameters, similar to those in
create_n2v_configand this will produce a valid configuration with all the other parameters set by the pydantic defaults. In init concrete builder classes must create a minimumconfig_dictattribute, defined by theTypedDictConfigDict. If a user wants to change any parameters from the defaults they have to call extra "builder methods".The "builder methods" build up the internal config dictionary incrementally until finally
buildis called and the final configuration is validated by pydantic and returned.The user is able to chain multiple of these methods by calling one after the other (see examples at the bottom), this is because each method returns a reference to self.
Mixins
I have decided to use a mixin mechanism to share sets of "builder methods" between multiple ConfigBuilder classes. Mixins can get confusing if you're not careful and can sometimes also be tricky to debug but I came to the conclusion that this would have the best trade-off between reusing code and user experience. The inherited methods are recognised by IDEs, and mkdocstrings can also render inherited members in the API reference.
Sets of "builder methods" that will likely be universal across all algorithms are included in the
TrainingParamsMixinandOptimizerParamsMixin, it is likely that most of theDataParamsMixinmethod will also be relevant for microsplit but we will probably also need some extra parameters.BaseConfigBuilder
The config builder classes should inherit from the
BaseConfigBuilder. There is also a protocolConfigBuilderand a type variable,ConfigBuilderT. The mixin's self argument has to be typed asConfigBuilderTfor IDEs to work properly.Additional hook
The base config builder also provides an additional hook
_before_build. This can be used by the mixins or the child config builders to make any last modifications to the config dict before pydantic validates it. However, we should probably try to use this sparingly to avoid any confusing behaviour. It will be called in the order that the child classes are subclassed.The
_before_buildhook is currently only used by theN2VConfigBuilderto set themonitor_metricin the checkpoint and early-stopping callback parameters because it has to wait until after theset_checkpoint_paramsandset_early_stopping_paramsis called.Things to note
None, this way if a parameter isNonethen we fall back to the defaults in the pydantic classes and we don't have to maintain two sets of defaults in theConfigBuilderclasses and the Pydantic classes.Changes Made
New features or files
Builders:
N2VConfigBuilderN2NConfigBuilderCAREConfigBuilderBaseConfigBuilderConfigBuilder(Protocol)Mixins:
DataParamsMixinTrainingParamsMixinOptimizerParamsMixinUnetParamsMixinHow has this been tested?
Only experimented in notebooks, there are probably still some bugs.
Related Issues
First proposed in #902
Additional Notes and Examples
How the API would look for some common use-cases:
And an example of if someone wanted to change at least one parameter in each build method (hopefully a very rare use-case)
Please ensure your PR meets the following requirements: