Files
Benjamin Bossan 4048dd3e83 Method comparison: Image benchmark shows sample images (#3309)
The Gradio space for the PEFT benchmarks already contains logic to
show the image generation results once the experiments have been run
and pushed to the repo. However, it doesn't contain any logic to show
generated sample images. Those are quite important, as the metrics are
not sufficient to estimate the quality of the fine-tuned model.

This PR adds a gallery element below the dataframe that shows sample
images. By default, it just shows the training images from the dataset
to give viewers an impression of what they're dealing with. If a user
clicks on a row in the dataframe, e.g. the row for LoRA, the five
sample images for that PEFT method are shown (if they can be found in
the HF bucket). There is a toggle for the user to switch between
showing generated sample images and training images.

Morever, in this PR, I made a change to use json.dumps to put the
configs into the dataframe instead of simple coercing them to
string. That makes it easier to parse the data.

I also noticed that in the dataframe, we would show the combined
metrics between all tasks, which included metrics that are not
shared. So e.g. for the image gen task, we would show test accuracy,
and for MetaMath, we would show DINO similarity. The PR changes the
logic to only show relevant metrics.

Moreover, the image gen metrics were on the far right of the
dataframe, making them hard to view. Now the order puts them further
to the left.

Finally, I added a short task description above the dataframe so that
viewers can understand at a glance what they're looking at. It
includes a link to the checkpoints on HF buckets.
2026-06-12 12:29:24 +02:00

344 lines
11 KiB
Python

# Copyright 2025-present the HuggingFace Inc. team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Data processing used for analyzing and presenting the results"""
import json
import os
import pandas as pd
_COMMON_METRIC_PREFERENCES = {
"accelerator_memory_reserved_avg": "lower",
"accelerator_memory_max": "lower",
"accelerator_memory_reserved_99th": "lower",
"total_time": "lower",
"train_time": "lower",
"file_size": "lower",
"train_loss": "lower",
"num_trainable_params": "lower",
}
_TASK_METRIC_PREFERENCES = {
"MetaMathQA": {
"test_accuracy": "higher",
"forgetting*": "lower",
},
"image-gen": {
"test_dino_similarity": "higher",
"drift*": "lower",
},
}
_TASK_PARETO_DEFAULTS = {
"MetaMathQA": ("accelerator_memory_max", "test_accuracy"),
"image-gen": ("accelerator_memory_max", "test_dino_similarity"),
}
_METRIC_EXPLANATIONS = {
"MetaMathQA": (
"*forgetting: This is the reduction in CE loss on a sample of Wikipedia data and reflects how much the "
"model 'forgot' during training. The lower the number, the better."
),
"image-gen": (
"*drift: This measures how much the generated images drift from the base model's outputs on unrelated "
"prompts, reflecting how much the model 'forgot' during training. The lower the number, the better."
),
}
def _get_metric_explanation(task_name):
return _METRIC_EXPLANATIONS.get(task_name, "")
def _preprocess_common(row):
"""Extract fields common to all tasks from a single result row.
Returns a tuple of metainfo dict and train metrics, or None if the row should be skipped.
"""
run_info = row["run_info"]
train_info = row["train_info"]
meta_info = row["meta_info"]
if run_info["peft_config"]:
peft_type = run_info["peft_config"]["peft_type"]
else:
peft_type = "full-finetuning"
if train_info["status"] != "success":
return None
train_metrics = train_info["metrics"][-1]
dct = {
"experiment_name": run_info["experiment_name"],
"model_id": run_info["train_config"]["model_id"],
"train_config": json.dumps(run_info["train_config"]),
"peft_type": peft_type,
"peft_config": json.dumps(run_info["peft_config"]),
"accelerator_memory_reserved_avg": train_info["accelerator_memory_reserved_avg"],
"accelerator_memory_max": train_info["accelerator_memory_max"],
"accelerator_memory_reserved_99th": train_info["accelerator_memory_reserved_99th"],
"total_time": run_info["total_time"],
"train_time": train_info["train_time"],
"file_size": train_info["file_size"],
"num_trainable_params": train_info["num_trainable_params"],
"train_loss": train_metrics["train loss"],
"train_samples": train_metrics["train samples"],
"peft_version": meta_info["package_info"]["peft-version"],
"peft_branch": run_info["peft_branch"],
"transformers_version": meta_info["package_info"]["transformers-version"],
"datasets_version": meta_info["package_info"]["datasets-version"],
"torch_version": meta_info["package_info"]["torch-version"],
"package_info": json.dumps(meta_info["package_info"]),
"system_info": json.dumps(meta_info["system_info"]),
"created_at": run_info["created_at"],
}
return dct, train_metrics
def _preprocess_metamathqa(dct, train_metrics, meta_info):
"""Add MetaMathQA-specific fields."""
dct["test_accuracy"] = train_metrics["test accuracy"]
dct["train_total_tokens"] = train_metrics["train total tokens"]
dct["forgetting*"] = train_metrics.get("forgetting", 123)
dct["bitsandbytes_version"] = meta_info["package_info"]["bitsandbytes-version"]
def _preprocess_image_gen(dct, train_metrics, meta_info):
"""Add image-gen-specific fields."""
dct["test_dino_similarity"] = train_metrics["test dino_similarity"]
dct["drift*"] = train_metrics.get("drift", 123)
dct["diffusers_version"] = meta_info["package_info"]["diffusers-version"]
_TASK_PREPROCESSORS = {
"MetaMathQA": _preprocess_metamathqa,
"image-gen": _preprocess_image_gen,
}
def format_df(df):
return df.style.format(precision=3, thousands=",", decimal=".")
def preprocess(rows, task_name: str, print_fn=print):
task_preprocessor = _TASK_PREPROCESSORS.get(task_name)
if task_preprocessor is None:
raise ValueError(f"Unknown task_name: {task_name!r}. Choose from {list(_TASK_PREPROCESSORS)}")
results = []
skipped = 0
for row in rows:
common = _preprocess_common(row)
if common is None:
skipped += 1
continue
dct, train_metrics = common
dct["task_name"] = task_name
task_preprocessor(dct, train_metrics, row["meta_info"])
results.append(dct)
if skipped:
print_fn(f"Skipped {skipped} of {len(rows)} entries because the train status != success")
return results
def load_jsons(path):
results = []
for fn in os.listdir(path):
if fn.endswith(".json"):
with open(os.path.join(path, fn)) as f:
row = json.load(f)
results.append(row)
return results
_COMMON_DTYPES = {
"task_name": "string",
"experiment_name": "string",
"model_id": "string",
"train_config": "string",
"peft_type": "string",
"peft_config": "string",
"accelerator_memory_reserved_avg": int,
"accelerator_memory_max": int,
"accelerator_memory_reserved_99th": int,
"total_time": float,
"train_time": float,
"file_size": int,
"train_loss": float,
"train_samples": int,
"num_trainable_params": int,
"peft_version": "string",
"peft_branch": "string",
"transformers_version": "string",
"datasets_version": "string",
"torch_version": "string",
"package_info": "string",
"system_info": "string",
"created_at": "string",
}
_TASK_DTYPES = {
"MetaMathQA": {
"test_accuracy": float,
"train_total_tokens": int,
"forgetting*": float,
"bitsandbytes_version": "string",
},
"image-gen": {
"test_dino_similarity": float,
"drift*": float,
"diffusers_version": "string",
},
}
_TASK_IMPORTANT_COLUMNS = {
"MetaMathQA": [
"experiment_name",
"peft_type",
"total_time",
"train_time",
"test_accuracy",
"train_loss",
"accelerator_memory_max",
"accelerator_memory_reserved_99th",
"accelerator_memory_reserved_avg",
"num_trainable_params",
"file_size",
"created_at",
"task_name",
"forgetting*",
],
"image-gen": [
"experiment_name",
"peft_type",
"total_time",
"train_time",
"test_dino_similarity",
"drift*",
"train_loss",
"accelerator_memory_max",
"accelerator_memory_reserved_99th",
"accelerator_memory_reserved_avg",
"num_trainable_params",
"file_size",
"created_at",
"task_name",
],
}
def get_task_columns(task_name):
"""Return the columns relevant to a task, ordered for display.
The important columns (including the task's own metrics) come first, followed by the remaining columns. Columns
belonging to other tasks are excluded.
"""
relevant = list(_COMMON_DTYPES) + list(_TASK_DTYPES.get(task_name, {}))
important = _TASK_IMPORTANT_COLUMNS.get(task_name, ["experiment_name", "peft_type"])
ordered = [col for col in important if col in relevant]
ordered += [col for col in relevant if col not in ordered]
return ordered
def load_df(path, task_name, print_fn=print):
jsons = load_jsons(path)
preprocessed = preprocess(jsons, task_name=task_name, print_fn=print_fn)
dtype_dict = {**_COMMON_DTYPES, **_TASK_DTYPES.get(task_name, {})}
if not preprocessed:
return pd.DataFrame(columns=dtype_dict.keys())
df = pd.DataFrame(preprocessed)
df = df.astype(dtype_dict)
df["created_at"] = pd.to_datetime(df["created_at"])
# round training time to nearest second
df["train_time"] = df["train_time"].round().astype(int)
df["total_time"] = df["total_time"].round().astype(int)
# reorder columns for better viewing, pinned_columns arg in Gradio seems not to work correctly
df = df[get_task_columns(task_name)]
columns = ["experiment_name", "model_id", "peft_type", "created_at"]
# we want to keep only the most recent run for each experiment
df = df.sort_values("created_at").drop_duplicates(columns, keep="last")
return df
def get_metric_preferences(task_name):
prefs = dict(_COMMON_METRIC_PREFERENCES)
prefs.update(_TASK_METRIC_PREFERENCES.get(task_name, {}))
return prefs
def get_model_ids(task_name, df):
filtered = df[df["task_name"] == task_name]
return sorted(filtered["model_id"].unique())
def filter_data(task_name, model_id, df):
filtered = df[(df["task_name"] == task_name) & (df["model_id"] == model_id)]
# only show the columns relevant to the task, with the important ones first
return filtered[get_task_columns(task_name)]
# Compute the Pareto frontier for two selected metrics.
def compute_pareto_frontier(df, metric_x, metric_y, metric_preferences):
if df.empty:
return df
df = df.copy()
points = df[[metric_x, metric_y]].values
selected_indices = []
def dominates(a, b, metric_x, metric_y):
# Check for each metric whether b is as good or better than a
if metric_preferences[metric_x] == "higher":
cond_x = b[0] >= a[0]
better_x = b[0] > a[0]
else:
cond_x = b[0] <= a[0]
better_x = b[0] < a[0]
if metric_preferences[metric_y] == "higher":
cond_y = b[1] >= a[1]
better_y = b[1] > a[1]
else:
cond_y = b[1] <= a[1]
better_y = b[1] < a[1]
return cond_x and cond_y and (better_x or better_y)
for i, point in enumerate(points):
dominated = False
for j, other_point in enumerate(points):
if i == j:
continue
if dominates(point, other_point, metric_x, metric_y):
dominated = True
break
if not dominated:
selected_indices.append(i)
pareto_df = df.iloc[selected_indices]
return pareto_df
def load_task_results(task_configs):
dfs = []
for task_name, path in task_configs.items():
if os.path.isdir(path):
task_df = load_df(path, task_name=task_name)
if not task_df.empty:
dfs.append(task_df)
return pd.concat(dfs, ignore_index=True)