-
Notifications
You must be signed in to change notification settings - Fork 632
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
ENH Add update metadata to repocard #844
Conversation
The documentation is not available anymore as the PR was closed or merged. |
the behavior sounds reasonable to me, but isn't it weird in terms of API design that there's a special case for |
The main reason to do it different for Alternatively, we could outsource that logic to a helper function that grabs the existing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR @lvwerra .
In the future, please send PRs from your own branch. We're trying to cleanup branches on the main repo. The CI works here on PRs from fork repositories.
Co-authored-by: Adrin Jalali <adrin.jalali@gmail.com>
Code UpdateI refactored the
CasesGiven the following example for existing metadata on the hub under existing_results = [{'dataset': {'name': 'IMDb', 'type': 'imdb'},
'metrics': [{'name': 'Accuracy', 'type': 'accuracy', 'value': 0.995}],
'task': {'name': 'Text Classification', 'type': 'text-classification'}}] 1 Overwrite existing metric value in existing resultThis happens if the values of new_results = deepcopy(existing_results)
new_results[0]["metrics"][0]["value"] = 0.999
_update_metadata_model_index(existing_results, new_results, overwrite=True)
# result:
[{'dataset': {'name': 'IMDb', 'type': 'imdb'},
'metrics': [{'name': 'Accuracy', 'type': 'accuracy', 'value': 0.999}],
'task': {'name': 'Text Classification', 'type': 'text-classification'}}] 2 Add new metric to existing resultThis happens if the values of new_results = deepcopy(existing_results)
new_results[0]["metrics"][0]["name"] = "Recall"
new_results[0]["metrics"][0]["type"] = "recall"
# result:
[{'dataset': {'name': 'IMDb', 'type': 'imdb'},
'metrics': [{'name': 'Accuracy', 'type': 'accuracy', 'value': 0.995},
{'name': 'Recall', 'type': 'recall', 'value': 0.995}],
'task': {'name': 'Text Classification', 'type': 'text-classification'}}] 3 Add new resultThis happens if not both values of new_results = deepcopy(existing_results)
new_results[0]["dataset"] = {'name': 'IMDb-2', 'type': 'imdb_2'}
# result:
[{'dataset': {'name': 'IMDb', 'type': 'imdb'},
'metrics': [{'name': 'Accuracy', 'type': 'accuracy', 'value': 0.995}],
'task': {'name': 'Text Classification', 'type': 'text-classification'}},
{'dataset': ({'name': 'IMDb-2', 'type': 'imdb_2'},),
'metrics': [{'name': 'Accuracy', 'type': 'accuracy', 'value': 0.995}],
'task': {'name': 'Text Classification', 'type': 'text-classification'}}] Hope that clarifies what |
for new_result in new_results: | ||
result_found = False | ||
for existing_result_index, existing_result in enumerate(existing_results): | ||
if all( | ||
new_result[feat] == existing_result[feat] | ||
for feat in UNIQUE_RESULT_FEATURES | ||
): | ||
result_found = True | ||
existing_results[existing_result_index][ | ||
"metrics" | ||
] = _update_metadata_results_metric( | ||
new_result["metrics"], | ||
existing_result["metrics"], | ||
overwrite=overwrite, | ||
) | ||
if not result_found: | ||
existing_results.append(new_result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since dictionaries are mutable, you could also write this as:
try:
existing_result = next(
x
for x in existing_results
if all(x[feat] == new_result[feat] for feat in UNIQUE_RESULT_FEATURES)
)
existing_result["metrics"] = _update_metadata_results_metric(
new_result["metrics"],
existing_result["metrics"],
overwrite=overwrite,
)
except StopIteration:
existing_results.append(new_result)
which should be faster since it avoids one slow for loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this.
src/huggingface_hub/repocard.py
Outdated
for new_metric in new_metrics: | ||
metric_exists = False | ||
for existing_metric_index, existing_metric in enumerate(existing_metrics): | ||
if all( | ||
new_metric[feat] == existing_metric[feat] | ||
for feat in UNIQUE_METRIC_FEATURES | ||
): | ||
if overwrite: | ||
existing_metrics[existing_metric_index]["value"] = new_metric[ | ||
"value" | ||
] | ||
else: | ||
# if metric exists and value is not the same throw an error without overwrite flag | ||
if ( | ||
existing_metrics[existing_metric_index]["value"] | ||
!= new_metric["value"] | ||
): | ||
raise ValueError( | ||
f"""You passed a new value for the existing metric '{new_metric["name"]}'. Set `overwrite=True` to overwrite existing metrics.""" | ||
) | ||
metric_exists = True | ||
if not metric_exists: | ||
existing_metrics.append(new_metric) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could do the same here:
for new_metric in new_metrics:
try:
existing_metric = next(
x
for x in existing_metrics
if all(x[feat] == new_metric[feat] for feat in UNIQUE_METRIC_FEATURES)
)
if overwrite:
existing_metric["value"] = new_metric["value"]
else:
# if metric exists and value is not the same throw an error
# without overwrite flag
if existing_metric["value"] != new_metric["value"]:
existing_str = ",".join(
new_metric[feat] for feat in UNIQUE_METRIC_FEATURES
)
raise ValueError(
"You passed a new value for the existing metric"
f" '{existing_str}'. Set `overwrite=True` to overwrite existing"
" metrics."
)
except StopIteration:
existing_metrics.append(new_metric)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This as well :)
tests/test_repocard.py
Outdated
if os.path.exists(REPO_NAME): | ||
shutil.rmtree(REPO_NAME, onerror=set_write_permission_and_retry) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we clone the repo under a tempfile.mkdtemp
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also integrated tempfile.mkdtemp
everywhere.
Co-authored-by: Adrin Jalali <adrin.jalali@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, could you please merge with the latest main
and run black on these files again? Otherwise LGTM.
Done - thanks for reviewing and the helpful suggestions @adrinjalali! |
Nice. I'll wait for @osanseviero to have a look and merge. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks very neat! I left some minor suggestions and a couple of questions. Thanks for this PR 🔥 🔥
I wrote a colab while I was exploring this https://colab.research.google.com/drive/1fG8OWYTnI6ucnafYKtrf-HwxtWbVBVQh?usp=sharing
src/huggingface_hub/repocard.py
Outdated
existing_result = next( | ||
x | ||
for x in existing_results | ||
if all(x[feat] == new_result[feat] for feat in UNIQUE_RESULT_FEATURES) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assumes metadata in existing data is valid and has both datasets
and task
tags, it will break if it's not valid. How should we handle those scenarios? Should we validate model-index beforehand? Show an error message? Override invalid metadata?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we pull the metadata from the Hub can't we assume that it is valid? In your experiment pushing invalid metadata was rejected, right? So it should not be there in the first place if it is invalid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm @julien-c WDYT? https://huggingface.co/osanseviero/llama-horse-zebra
The result has task and metrics, but no associated dataset. This is not rejected by the server, and the evaluation results are nicely shown, with the only con that there is no associated dataset, so there's no leaderboard.
IMO this is still valid metadata which is just incomplete. Most spaCy models are like this https://huggingface.co/models?library=spacy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes we try to still display the eval results in this case even though it's not perfect
src/huggingface_hub/repocard.py
Outdated
except StopIteration: | ||
existing_results.append(new_result) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this really needed? I think try/except to do this is a bit less readable and could be more prone to introduce bugs. Maybe the for loop could be more explicit. I think that was how it was before, I personally find that more readable and easier to maintain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no strong opinion here - happy to revert. @adrinjalali?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The for loop is slower, and try/except clauses are quite pythonic. I do think the current state is quite better than a for loop. One can always add a bit of comment on when the exception is raised if that helps with readability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think speed is important here as we iterate through a dictionary with a handful of entries while pulling/pushing to the hub in the same function which is probably 1000x slower.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not optimize for execution speed of an update method that won't be called thousands in time per second at the moment, and would rather optimize for readability and consistency with the rest of the codebase. This implem iterates over new_results
with a for loop, and over existing_results
with a try/except/next approach. Should we start changing all nested loops to match this?
If you feel strong about this let's go with it though 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do find the for/loop implementation less readable. Removing the outer loop would make it quite complicated and not very readable, that's why I didn't suggest to remove it.
As for consistency, I don't think it's about staying consistent with the rest of the code. Sometimes a for loop makes sense, sometimes it doesn't. And as a team when we work on a codebase, sometimes we find better ways to do things, and that's fine, and I don't think we should hold back because we have done things in a certain way so far. To me it's okay to look at a codebase and notice old stuff vs new stuff. The way people do things changes and new code can look different than the old code and that's fine to me.
But if both of you think the for loop is more readable than this, then sure, change it. To me it's the other way around.
Co-authored-by: Omar Sanseviero <osanseviero@gmail.com>
I integrated the feedback and reverted back to for-loops for now. Can change it again if somebody has strong opinions. Let me know if this looks good now @osanseviero @adrinjalali. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Let's wait until other tests (unrelated to this PR ) to be fixed to submit on green 🚀
Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left a few small nits, other than that looks good to me!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Only left nits.
src/huggingface_hub/repocard.py
Outdated
repo_type (`str`, *optional*): | ||
Set to `"dataset"` or `"space"` if updating to a dataset or space, | ||
`None` or `"model"` if updating to a model. Default is `None`. | ||
overwrite (`bool`, *optional*): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overwrite (`bool`, *optional*): | |
overwrite (`bool`, *optional*, defaults to `False`): |
Co-authored-by: Julien Chaumond <julien@huggingface.co> Co-authored-by: Lysandre Debut <lysandre.debut@reseau.eseo.fr>
Thank you for addressing all comments! Merging. |
* add `metadata_update` function * add tests * add docstring * Apply suggestions from code review Co-authored-by: Adrin Jalali <adrin.jalali@gmail.com> * refactore `_update_metadata_model_index` * Apply suggestions from code review Co-authored-by: Adrin Jalali <adrin.jalali@gmail.com> * fix style and imports * switch to deepcopy everywhere * load repo in repocard test into tmp folder * simplify results and metrics checks when updating metadata * run black * Apply suggestions from code review Co-authored-by: Omar Sanseviero <osanseviero@gmail.com> * fix pyyaml version to work with `sort_keys` kwarg * don't allow empty commits if file hasn't changed * switch order of updates to first check model-index for easier readbility * expose repocard functions through `__init__` * fix init * make style & quality * revert to for-loop * Apply suggestions from code review Co-authored-by: Julien Chaumond <julien@huggingface.co> Co-authored-by: Lysandre Debut <lysandre.debut@reseau.eseo.fr> * post suggestion fixes * add example * add type to list Co-authored-by: Adrin Jalali <adrin.jalali@gmail.com> Co-authored-by: Omar Sanseviero <osanseviero@gmail.com> Co-authored-by: Julien Chaumond <julien@huggingface.co> Co-authored-by: Lysandre Debut <lysandre.debut@reseau.eseo.fr>
This PR adds a
metadata_update
function that allows the user to update the metadata in a repository on the hub. The function accepts a dict with metadata (following the same pattern as the YAML in the README) and behaves as follows for all top level fields exceptmodel-index
:overwrite=True
is passed as a safety guard.For
model-index
the behaviour is more nuanced:task
anddataset
exist, thenoverwrite=True
)task
anddataset
does not exist, the result is appended to the resultsFor reference this is an example of a model's metadata structure as a dictionary:
One minor issue I found is that I need to use
force_download=True
for the tests to pass as otherwise thehf_hub_download
uses the cached but outdated version of the README even if the README has been updated on the remote. cc @LysandreJikhuggingface_hub/src/huggingface_hub/repocard.py
Line 137 in a191950
This feature will be used for huggingface/evaluate#6 and closes #835.