Closed shenmishajing closed 2 years ago
Hi @shenmishajing, thanks for the thoughtful feature request.
This proposal fits better in jsonargparse
, the library that the LightningCLI
uses for parsing.
You can open the same request in their GitHub repository: https://github.com/omni-us/jsonargparse/issues
@shenmishajing indeed if a proposal is not specific to what LightningCLI
does, i.e. it applies in general to other parsing use cases, then it should be a request in the jsonargparse
repo. Having said this, I do have a few comments:
LightningCLI
already has options that support multiple config files.
cli.py --config default_runtime.yaml --data datasets/xxx.yaml --model models/xxx.yaml
.Different sections can be referenced in a common config file, i.e. a command as cli.py --config config.yaml
with a corresponding yaml as
data: datasets/xxx.yaml
model: models/xxx.yaml
...
Having additional keywords such as __base__
and __import__
steepens the learning curve and in my opinion makes it harder to write and remember.
The motivation seems a bit unfounded. Why is it a problem that the command is long? Is it the effort to write this command? In a shell normally there is history and standard keyboard short cuts that help modifying previous commands with little effort. Furthermore, the command itself could be part of a shell script. Modifying part of the command in a shell script in terms of effort is not greater than modifying it in an additional config file.
Having different ideas from people to improve things is great, so thank you for posting. I hope you do propose improvements in jsonargparse
or here. My comments are intended to be constructive and give you a broader perspective.
@mauvilsa thanks for your reply. I will create a pr in jsonargparse
repo. And for your comments:
When we do some research, we may design a model and run it with many combines of hyper-params. hyper-params are similar to each other, but some part of them are different.
So if we use the raw CLI, we have to write many config files for every combine of hyper-param(otherwise, we have to change some params in shell command. It will be tiresome when there are more and more params), then if we change some common params, we have to change every config files.
But we have the __base__
feature, we can create a common config file as a base file, and use __base__
feature to write the other combine of params, then we only have to change the base config file if we want to change some common param. It will make less mistake in config.
As for this feature steepens the learning curve, we can do not use it, if we do not have the motivation to use it. So, I do not think it will steepens the learning curve or make more mistake etc.
Thanks for your reply and suggestion again.
@shenmishajing I described two already existing features that to my understanding already support your need. Not sure if you didn't understand how to use them of if there is some detail about your use case which I didn't understand and makes these not practical. Please ask if more clarification is needed or explain better your use case. Introducing new special keywords in the config will likely not happen unless there is a compelling reason which has been very clearly explained and motivated and is not already achievable with already existing features.
@mauvilsa I have already understood the two way you described, but them do not support my need. I will give some use case.
Contrast experiment is a common use case in research. In general, we have to run many experiments with same hyper-params except only one or a little bit params. It will looks like this:
model:
param1: value1
common: part
some_thing: else
model:
param1: value2
common: part
some_thing: else
model:
param1: value3
common: part
some_thing: else
But, after this contrast experiment, we also may want to change some param to rerun this experiment some times, casued by that some params we set are not the best param or we want to add another experiment. Therefore, we want the config files look like this:
model:
param1: value1
common: part_with_different_value
some_thing: else
model:
param1: value2
common: part_with_different_value
some_thing: else
model:
param1: value3
common: part_with_different_value
some_thing: else
But, if there are many runs, it will be fallible for us to change every config files, especially when we have many contrast experiments, or we want to rerun an experiment several weeks ago because the config file do not have hierarchical structure and inheritance information.
But, by the __base__
key word, we can write config files like this:
model:
param1: value1
common: part
some_thing: else
__base__: ./Base.yaml
model:
param1: value1
__base__: ./Base.yaml
model:
param1: value2
__base__: ./Base.yaml
model:
param1: value3
Therefore, if we want to change the common part, we only have to change the Base.yaml
config file, it will be less probability to make mistake. And we can get the purpose of the constract experiment even after several weeks, because config files have hierarchical structure and inheritance information, the params different is written in every run config files clearly.
P.S. param1 and common keys are just a represtation, they may be several keys in practice, so writing them in command line is not a good idea.
There are also some cases, in which our datasets or models have multi modes, like grayscale or RGB dataset, align or not align multi modality data. So we may define the dateset and models like this:
dataset:
color_type: grayscale
common: part
some_thing: else
dataset:
color_type: RGB
common: part
some_thing: else
Also, if we want to change the common part, we have to change the two config files. If we have to add some new params, we have to change the two config files, like this:
dataset:
color_type: grayscale
new_param: new_value
common: part_with_different_value
some_thing: else
dataset:
color_type: RGB
new_param: new_value
common: part_with_different_value
some_thing: else
By __base__
keyword, we can write them like this:
dataset:
color_type: grayscale
common: part
some_thing: else
__base__: ./Base.yaml
dataset:
color_type: grayscale
__base__: ./Base.yaml
dataset:
color_type: RGB
And we only have to change the Base.yaml
to change the common part and add the new param:
dataset:
color_type: grayscale
new_param: new_value
common: part_with_different_value
some_thing: else
It will be less probability to make mistake.
This use case is not general as the first two, but it is very cool I think.
Sometimes, we want to try out different backbones for our model or we want to use an existing model as a part of our model, or we want to use a part of an existing model as a part of our model. We can do it with __base__
keyword.
model:
class_path: models.ResNet50
init_args:
in_channels: 3
model:
backbone:
__base__:
- [ ./ResNet.yaml, model ]
I have not push the code of this part to github now, if you are instereted in it, I will update it soon.
In summary, the __base__
keyword construct the hierarchical structure and inheritance information of config files, it help us to get the purpose of every config file as soon as possible, and help us make less mistake.
And, the reason why current feature can not support my need is that they can not change some part directly.
If we use the way as cli.py --config default_runtime.yaml --data datasets/xxx.yaml --model models/xxx.yaml
, we have to write the every param we want to change in command, it will be failible and weary.
If we use a single config file like:
data: datasets/xxx.yaml
model: models/xxx.yaml
...
We can not change some param in this file, which means we can not write files like:
data: datasets/xxx.yaml
model: models/xxx.yaml
param1: a_new_value
...
So, these two ways will lead to write many and many config files without hierarchical structure and inheritance information, which means hard to manage them and get the purpose of each single file after several days.
@shenmishajing what you want is already supported.
You would have a base config like:
# Base.yaml
model:
param1: value0
common: part
some_thing: else
Then you have a second config which overrides a few of the values from the base, for example:
# Run.yaml
model:
param1: value1
The command would be run as:
cli.py --config Base.yaml --config Run.yaml
Regarding your comments:
Therefore, if we want to change the common part, we only have to change the Base.yaml config file, it will be less probability to make mistake.
You can do the same with what is already supported.
And we can get the purpose of the constract experiment even after several weeks, because config files have hierarchical structure and inheritance information, the params different is written in every run config files clearly.
I am not entirely sure what you mean. If you change the Base.yaml
file across different runs and if for each run you save its run yaml which had the __base__: ./Base.yaml
then it is not possible to know what version of Base.yaml
was used. Also note that in LightningCLI
the full config is always saved automatically in the log directory. This is what can really be trusted to know what was done for each run. Comparing runs is just simple diffs of the configs.
Your second use case for the dataset seems to be the same as the first.
This is also already supported, though it might depend on what exactly you are doing.
Assuming that your model has a backbone
init parameter, similar to the first use case you would reference another config:
model:
backbone: ResNet.yaml
And the file ResNet.yaml
would look like:
class_path: models.ResNet50
init_args:
in_channels: 3
The LightningCLI
has the parameter save_config_multifile
which if set to True
when saving the config to the log directory it will preserve this structure instead of being a single big config.
@mauvilsa I used this way before, but it is failible also. The different between inhert config file in file and in command is that we have no to remeber which config file should inhert from which config file.
There will be many hyper-params for a single model, so we will design several experiments to find the best combine of hyper-params. If we do not define the inhert relation in config file, the run command will look like this:
# first experiment
cil.py --config models/base.yaml --config models/experiments_1/run_1.yaml
# second experiment
cil.py --config models/base.yaml --config models/experiments_1/run_best.yaml --config models/experiments_2/run_1.yaml
The third experiment may be some params different.
# third experiment
cil.py --config models/base.yaml --config models/experiments_1/run_different.yaml --config models/experiments_2/run_best.yaml --config models/experiments_3/run_1.yaml
and so on. If we add the config part of dataset and trainer and other things, the command will be too long to edit and too much inhert structure to remeber.
In fact, I want to make the ResNet.yaml also a whole config file to run. A more clear use case may be like this:
Two mode with same backbone:
# model_1
model:
backbone:
class_path: models.ResNet50
init_args:
in_channels: 3
some_thing: else
# model_2
model:
backbone:
__base__:
- ./model_1.yaml: model.backbone
another: things
Do this as follows:
# ResNet.yaml
class_path: models.ResNet50
init_args:
in_channels: 3
# model_1
model:
backbone: ResNet.yaml
some_thing: else
# model_2
model:
backbone: ResNet.yaml
another: things
One person wanting to do it differently even though there are similar alternatives is not enough to add it as a new feature. Even more if this introduces new syntax such as ./model_1.yaml: model.backbone
. You are free to open this as a feature request in jsonargparse, but it will not be worked on or merged unless there is a large group of people wanting this despite existing features.
Now I see that your use case is that you have an increasing number of config files that should be applied one after the other. But you want to specify them in a config file so that the number of --config
command line arguments does not increase. You can create this as a feature request in jsonargparse. But don't mix this with the Use case 3.
By the way, what you were doing in the pull request referenced in this issue is not the way how this would be implemented. It shouldn't be a modification of the ActionConfigFile
class. Also a merge of configs is not a simple deep merge of dicts, this will break some jsonargparse features. In reality this is more complex than what you imagine.
@mauvilsa thanks for your reply and suggestion, I will create a fr in jsonargparse.
Is there a way to support adding args to objects that exist in a list in a second yaml file?
for example if I have a lightningCLI trainer with a default config
LightningCLI(default_config_file
= default.yaml)`
and a dataset class like this:
class DataSetClass:
def __init__(path_to_data = Path)
and the default config looks something like
trainer:
...
datamodule:
datasets:
- mydatasets.DataSetClass
normally I can modify the init args of this DataSetClass in the default yaml via:
cli.py fit --datamodule.datasets.path_to_data /path/to/data
but I am trying to figure out how I can write out the --datamodule.datasets.path_to_data
arg in a second yaml file so it can get written into the args in the list's data object
It is possible to concatenate to a list, see list-append docs.
🚀 Feature
Add ability to include other config file when we use LightningCLI and config arg.
Motivation
If we want to train and test many models with same/many datasets, we may want to split the config file into many parts like trainer part, model part and dataset part. And then, the command to run our model will look like this:
LightningCLI allow us to include many config files in the same time, but the command will be too long to write. Therefore, we can write another config file looks like:
configs/runs/xxx.yaml:
Then, the command to run our model will look like:
It will be easier to write and remeber.
Alternatives
in pytorch_lightning/utilities/cli.py line 125
class LightningArgumentParser add argument --config with action ActionConfigFile from jsonargparse package, and we need override this action.
Additional context
The code of pop 'import' from config file is for easier use of reference of yaml file. We can write the yaml file more clearly using it. An example is when we use the dataset, we may want to define the transforms in the config files, too. And by this little trick, we can make the transforms more clearly like:
We can do it without this feature in this little case too, but the config file may be ugly and misleading.
If you enjoy Lightning, check out our other projects! âš¡
Metrics: Machine learning metrics for distributed, scalable PyTorch applications.
Lite: enables pure PyTorch users to scale their existing code on any kind of device while retaining full control over their own loops and optimization logic.
Flash: The fastest way to get a Lightning baseline! A collection of tasks for fast prototyping, baselining, fine-tuning, and solving problems with deep learning.
Bolts: Pretrained SOTA Deep Learning models, callbacks, and more for research and production with PyTorch Lightning and PyTorch.
Lightning Transformers: Flexible interface for high-performance research using SOTA Transformers leveraging Pytorch Lightning, Transformers, and Hydra.
cc @borda @carmocca @mauvilsa