modelmark 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modelmark/__init__.py +3 -0
- modelmark/__main__.py +5 -0
- modelmark/common/dataset.py +116 -0
- modelmark/common/downloader.py +88 -0
- modelmark/common/loader.py +63 -0
- modelmark/common/logger.py +34 -0
- modelmark/common/parser.py +134 -0
- modelmark/common/report.py +734 -0
- modelmark/common/tester.py +162 -0
- modelmark/common/utils.py +105 -0
- modelmark/config.py +97 -0
- modelmark/constants.py +34 -0
- modelmark/modelmark.py +206 -0
- modelmark/models/conv.py +75 -0
- modelmark/models/gru.py +45 -0
- modelmark/models/linear.py +65 -0
- modelmark/models/lstm.py +43 -0
- modelmark/models/transformer.py +86 -0
- modelmark-0.1.0.dist-info/METADATA +109 -0
- modelmark-0.1.0.dist-info/RECORD +24 -0
- modelmark-0.1.0.dist-info/WHEEL +5 -0
- modelmark-0.1.0.dist-info/entry_points.txt +2 -0
- modelmark-0.1.0.dist-info/licenses/LICENSE +21 -0
- modelmark-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
from html import escape
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
from playwright.sync_api import sync_playwright
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
console = Console()
|
|
19
|
+
from modelmark.common.utils import load_config
|
|
20
|
+
config = load_config()
|
|
21
|
+
|
|
22
|
+
class Report:
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
This class forms a paper-style table based on model evaluation data.
|
|
26
|
+
And saves results in both .html and .png formats that are easy to embed to any research paper or a website.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
|
|
31
|
+
self.html_path = "result.html"
|
|
32
|
+
self.png_path = "result.png"
|
|
33
|
+
self.machine_info = self._get_machine_info()
|
|
34
|
+
|
|
35
|
+
def _extract_mean(self, value) -> float:
|
|
36
|
+
"""Extract the mean from values such as '0.3561 ± 0.0275'."""
|
|
37
|
+
if isinstance(value, str):
|
|
38
|
+
return float(value.split("±")[0].strip())
|
|
39
|
+
return float(value)
|
|
40
|
+
|
|
41
|
+
def _highlight_best(
|
|
42
|
+
self,
|
|
43
|
+
row: "pd.Series",
|
|
44
|
+
columns: Iterable[tuple],
|
|
45
|
+
higher_is_better: set[str] | bool = False,
|
|
46
|
+
) -> set[tuple]:
|
|
47
|
+
"""
|
|
48
|
+
Given one row (a Series indexed by (model, metric) tuples) and the
|
|
49
|
+
(model, metric) column tuples that actually exist for this row,
|
|
50
|
+
return the set of (model, metric) tuples holding the best value
|
|
51
|
+
for each metric.
|
|
52
|
+
|
|
53
|
+
higher_is_better:
|
|
54
|
+
- False (default): every metric is "lower is better"
|
|
55
|
+
- True: every metric is "higher is better"
|
|
56
|
+
- a set of metric names: only those metrics are treated as
|
|
57
|
+
"higher is better"; everything else is "lower is better"
|
|
58
|
+
|
|
59
|
+
Only compares columns that actually exist for this row, so models
|
|
60
|
+
with different metric sets never cause a KeyError.
|
|
61
|
+
"""
|
|
62
|
+
best: set[tuple] = set()
|
|
63
|
+
|
|
64
|
+
by_metric: dict[str, list[tuple]] = {}
|
|
65
|
+
for model, metric in columns:
|
|
66
|
+
by_metric.setdefault(metric, []).append((model, metric))
|
|
67
|
+
|
|
68
|
+
for metric, cols in by_metric.items():
|
|
69
|
+
values = {}
|
|
70
|
+
for col in cols:
|
|
71
|
+
try:
|
|
72
|
+
values[col] = self._extract_mean(row[col])
|
|
73
|
+
except (ValueError, TypeError, KeyError):
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
if not values:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
want_max = higher_is_better is True or (
|
|
80
|
+
isinstance(higher_is_better, set) and metric in higher_is_better
|
|
81
|
+
)
|
|
82
|
+
best_col = max(values, key=values.get) if want_max else min(values, key=values.get)
|
|
83
|
+
best.add(best_col)
|
|
84
|
+
|
|
85
|
+
return best
|
|
86
|
+
|
|
87
|
+
def _ensure_contiguous_datasets(self, df: "pd.DataFrame") -> "pd.DataFrame":
|
|
88
|
+
"""
|
|
89
|
+
Reorder rows so every row belonging to the same top-level index
|
|
90
|
+
value ("Dataset") is contiguous, preserving first-appearance order
|
|
91
|
+
of datasets and original order within each dataset.
|
|
92
|
+
|
|
93
|
+
Required for HTML rowspan on the dataset cell to render correctly
|
|
94
|
+
— a rowspan just covers the next N rows in document order, so if
|
|
95
|
+
a dataset's rows aren't contiguous, the span will visually cover
|
|
96
|
+
rows belonging to a different dataset.
|
|
97
|
+
"""
|
|
98
|
+
dataset_order = list(dict.fromkeys(df.index.get_level_values(0)))
|
|
99
|
+
if len(dataset_order) <= 1:
|
|
100
|
+
return df
|
|
101
|
+
return pd.concat([df.xs(d, level=0, drop_level=False) for d in dataset_order])
|
|
102
|
+
|
|
103
|
+
def _build_stats_rows(
|
|
104
|
+
self,
|
|
105
|
+
model_stats: dict,
|
|
106
|
+
models: list,
|
|
107
|
+
metrics_by_model: dict,
|
|
108
|
+
highlight_best: bool,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Build one summary row per (label -> per-model value) entry in
|
|
112
|
+
model_stats. Unlike the main body, these are per-model, not
|
|
113
|
+
per-metric — e.g. training time, parameter count, GFLOPs — so
|
|
114
|
+
each value cell spans all of that model's metric columns.
|
|
115
|
+
|
|
116
|
+
Each entry in model_stats is either:
|
|
117
|
+
- a list/array of values, one per model, in the same order
|
|
118
|
+
as `models` (i.e. the order models appear left-to-right
|
|
119
|
+
in the table), or
|
|
120
|
+
- a dict mapping model name -> value.
|
|
121
|
+
|
|
122
|
+
highlight_best bolds the smallest value per row (time, params,
|
|
123
|
+
and FLOPs are always "smaller is better").
|
|
124
|
+
"""
|
|
125
|
+
rows_html = []
|
|
126
|
+
|
|
127
|
+
for label, values in model_stats.items():
|
|
128
|
+
if isinstance(values, dict):
|
|
129
|
+
by_model = values
|
|
130
|
+
else:
|
|
131
|
+
values = list(values)
|
|
132
|
+
if len(values) != len(models):
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"model_stats[{label!r}] has {len(values)} values but there are "
|
|
135
|
+
f"{len(models)} models ({models}); pass one value per model, in "
|
|
136
|
+
f"that order, or a dict keyed by model name."
|
|
137
|
+
)
|
|
138
|
+
by_model = dict(zip(models, values))
|
|
139
|
+
|
|
140
|
+
numeric = {}
|
|
141
|
+
for model in models:
|
|
142
|
+
try:
|
|
143
|
+
numeric[model] = self._extract_mean(by_model[model])
|
|
144
|
+
except (ValueError, TypeError, KeyError):
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
best_models = set()
|
|
148
|
+
if highlight_best and numeric:
|
|
149
|
+
best_val = min(numeric.values())
|
|
150
|
+
best_models = {m for m, v in numeric.items() if v == best_val}
|
|
151
|
+
|
|
152
|
+
cells = [f'<th class="stats-label" colspan="2">{escape(str(label))}</th>']
|
|
153
|
+
for model in models:
|
|
154
|
+
colspan = len(metrics_by_model[model])
|
|
155
|
+
classes = ["stats-value"]
|
|
156
|
+
if model in best_models:
|
|
157
|
+
classes.append("best")
|
|
158
|
+
value = by_model.get(model, "")
|
|
159
|
+
cells.append(
|
|
160
|
+
f'<td class="{" ".join(classes)}" colspan="{colspan}">{escape(str(value))}</td>'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
rows_html.append('<tr class="stats-row">' + "".join(cells) + "</tr>")
|
|
164
|
+
|
|
165
|
+
return "".join(rows_html)
|
|
166
|
+
|
|
167
|
+
def dataframe_to_html(
|
|
168
|
+
self,
|
|
169
|
+
df: "pd.DataFrame",
|
|
170
|
+
model_stats: dict,
|
|
171
|
+
metadata: dict,
|
|
172
|
+
title: str = "Neural Network Model Evaluation",
|
|
173
|
+
subtitle: str | None = None,
|
|
174
|
+
higher_is_better: set[str] | bool = False,
|
|
175
|
+
zebra_stripe: bool = True,
|
|
176
|
+
highlight_best_stats: bool = True,
|
|
177
|
+
) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Create a complete standalone HTML report from a DataFrame.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
df:
|
|
184
|
+
The multi-index dataframe with 2 columns and rows levels,
|
|
185
|
+
should be formed as in the example.
|
|
186
|
+
model_stats:
|
|
187
|
+
Per-model summary rows appended below the results,
|
|
188
|
+
e.g. training time / parameter count / GFLOPs — one value per
|
|
189
|
+
model rather than per metric, so each value spans that
|
|
190
|
+
model's whole column group. Example:
|
|
191
|
+
|
|
192
|
+
model_stats={
|
|
193
|
+
"Time (s)": [12.3, 9.8],
|
|
194
|
+
"Params (M)": [25.6, 22.1],
|
|
195
|
+
"GFLOPs": [4.1, 3.9],
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
Each list must be in the same order as the models appear in
|
|
199
|
+
the table (left to right); a dict of {model_name: value} is
|
|
200
|
+
also accepted per row if you'd rather not rely on order.
|
|
201
|
+
metadata:
|
|
202
|
+
The dictonary with system and hardware info.
|
|
203
|
+
title:
|
|
204
|
+
The name of the table or research.
|
|
205
|
+
subtitle:
|
|
206
|
+
Optional additional info about the research-related details.
|
|
207
|
+
html_path:
|
|
208
|
+
Path to the file that should contain the generated html code.
|
|
209
|
+
higher_is_better:
|
|
210
|
+
Passed through to the best-value highlighting. False (default)
|
|
211
|
+
treats every metric as lower-is-better; pass True to flip all
|
|
212
|
+
metrics, or a set of metric names to flip only those (e.g.
|
|
213
|
+
{"Accuracy", "F1"}).
|
|
214
|
+
zebra_stripe:
|
|
215
|
+
Light alternating row shading for on-screen readability.
|
|
216
|
+
Set False for a stricter print/booktabs look.
|
|
217
|
+
highlight_best_stats:
|
|
218
|
+
Bold the smallest value in each model_stats row (time,
|
|
219
|
+
params, and FLOPs are always "smaller is better").
|
|
220
|
+
|
|
221
|
+
The function writes the HTML file and returns the generated HTML.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
html_path = self.html_path
|
|
225
|
+
html_path = Path(html_path)
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------
|
|
228
|
+
# Validate DataFrame structure
|
|
229
|
+
# ---------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
if not isinstance(df.index, pd.MultiIndex):
|
|
232
|
+
raise ValueError("DataFrame index must be a MultiIndex, e.g. ['Dataset', 'Context'].")
|
|
233
|
+
|
|
234
|
+
if not isinstance(df.columns, pd.MultiIndex):
|
|
235
|
+
raise ValueError("DataFrame columns must be a MultiIndex, e.g. ['Model', 'Metric'].")
|
|
236
|
+
|
|
237
|
+
if df.index.nlevels != 2:
|
|
238
|
+
raise ValueError(f"Expected 2 index levels, got {df.index.nlevels}.")
|
|
239
|
+
|
|
240
|
+
if df.columns.nlevels != 2:
|
|
241
|
+
raise ValueError(f"Expected 2 column levels, got {df.columns.nlevels}.")
|
|
242
|
+
|
|
243
|
+
df = self._ensure_contiguous_datasets(df)
|
|
244
|
+
|
|
245
|
+
# Names used in the table (display labels only — lookups below
|
|
246
|
+
# are positional, so these can be anything, including None).
|
|
247
|
+
column_name_1 = df.columns.names[0] or "Model"
|
|
248
|
+
column_name_2 = df.columns.names[1] or "Metric"
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------
|
|
251
|
+
# Header
|
|
252
|
+
# ---------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
model_values = list(df.columns.get_level_values(0))
|
|
255
|
+
models = list(dict.fromkeys(model_values)) # preserve order
|
|
256
|
+
|
|
257
|
+
metrics_by_model = {}
|
|
258
|
+
for model in models:
|
|
259
|
+
metrics_by_model[model] = list(
|
|
260
|
+
dict.fromkeys(metric for current_model, metric in df.columns if current_model == model)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# First header row: model names, each with a short "cmidrule"
|
|
264
|
+
# underline spanning only its own metric columns.
|
|
265
|
+
header_row_1 = f"""
|
|
266
|
+
<tr class="header-main">
|
|
267
|
+
<th class="index-header corner-label" colspan="2">{escape(str(column_name_1))}</th>
|
|
268
|
+
"""
|
|
269
|
+
for model in models:
|
|
270
|
+
colspan = len(metrics_by_model[model])
|
|
271
|
+
header_row_1 += f"""
|
|
272
|
+
<th colspan="{colspan}" class="model-header">
|
|
273
|
+
<div class="model-header-inner">{escape(str(model))}</div>
|
|
274
|
+
</th>
|
|
275
|
+
"""
|
|
276
|
+
header_row_1 += "</tr>"
|
|
277
|
+
|
|
278
|
+
# Second header row: metric names, with a full-width rule beneath
|
|
279
|
+
# separating the header from the body.
|
|
280
|
+
header_row_2 = f"""
|
|
281
|
+
<tr class="header-metric">
|
|
282
|
+
<th class="corner-placeholder corner-label" colspan="2">{escape(str(column_name_2))}</th>
|
|
283
|
+
"""
|
|
284
|
+
for model in models:
|
|
285
|
+
for metric in metrics_by_model[model]:
|
|
286
|
+
header_row_2 += f"""
|
|
287
|
+
<th>{escape(str(metric))}</th>
|
|
288
|
+
"""
|
|
289
|
+
header_row_2 += "</tr>"
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------
|
|
292
|
+
# Body
|
|
293
|
+
# ---------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
body_rows = []
|
|
296
|
+
|
|
297
|
+
dataset_rowspans = {}
|
|
298
|
+
for dataset, group in df.groupby(level=0, sort=False):
|
|
299
|
+
dataset_rowspans[dataset] = len(group)
|
|
300
|
+
|
|
301
|
+
dataset_rendered = set()
|
|
302
|
+
previous_dataset = None
|
|
303
|
+
|
|
304
|
+
for row_number, (index, row) in enumerate(df.iterrows()):
|
|
305
|
+
dataset = index[0]
|
|
306
|
+
context = index[1]
|
|
307
|
+
|
|
308
|
+
best_columns = self._highlight_best(row, df.columns, higher_is_better=higher_is_better)
|
|
309
|
+
|
|
310
|
+
cells = []
|
|
311
|
+
|
|
312
|
+
# Dataset cell — only on the first row of each dataset block,
|
|
313
|
+
# spanning all of that dataset's rows.
|
|
314
|
+
if dataset not in dataset_rendered:
|
|
315
|
+
rowspan = dataset_rowspans[dataset]
|
|
316
|
+
cells.append(
|
|
317
|
+
f'<th class="dataset-cell" rowspan="{rowspan}">{escape(str(dataset))}</th>'
|
|
318
|
+
)
|
|
319
|
+
dataset_rendered.add(dataset)
|
|
320
|
+
|
|
321
|
+
# Context cell
|
|
322
|
+
cells.append(f'<th class="context-cell">{escape(str(context))}</th>')
|
|
323
|
+
|
|
324
|
+
# Metric cells
|
|
325
|
+
for model in models:
|
|
326
|
+
for metric in metrics_by_model[model]:
|
|
327
|
+
value = row[(model, metric)]
|
|
328
|
+
classes = ["value-cell"]
|
|
329
|
+
if (model, metric) in best_columns:
|
|
330
|
+
classes.append("best")
|
|
331
|
+
cells.append(f'<td class="{" ".join(classes)}">{escape(str(value))}</td>')
|
|
332
|
+
|
|
333
|
+
row_classes = []
|
|
334
|
+
if zebra_stripe and row_number % 2 == 1:
|
|
335
|
+
row_classes.append("striped")
|
|
336
|
+
if previous_dataset is not None and dataset != previous_dataset:
|
|
337
|
+
row_classes.append("dataset-start")
|
|
338
|
+
previous_dataset = dataset
|
|
339
|
+
|
|
340
|
+
body_rows.append(f'<tr class="{" ".join(row_classes)}">' + "".join(cells) + "</tr>")
|
|
341
|
+
|
|
342
|
+
# ---------------------------------------------------------
|
|
343
|
+
# Per-model summary rows (Time, Params, GFLOPs, ...)
|
|
344
|
+
# ---------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
stats_rows_html = ""
|
|
347
|
+
if model_stats:
|
|
348
|
+
stats_rows_html = self._build_stats_rows(model_stats, models, metrics_by_model, highlight_best_stats)
|
|
349
|
+
|
|
350
|
+
# ---------------------------------------------------------
|
|
351
|
+
# Metadata
|
|
352
|
+
# ---------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
metadata_rows = []
|
|
355
|
+
for key, value in metadata.items():
|
|
356
|
+
metadata_rows.append(
|
|
357
|
+
f"""
|
|
358
|
+
<div class="metadata-row">
|
|
359
|
+
<div class="metadata-label">{escape(str(key))}</div>
|
|
360
|
+
<div class="metadata-value">{escape(str(value))}</div>
|
|
361
|
+
</div>
|
|
362
|
+
"""
|
|
363
|
+
)
|
|
364
|
+
metadata_html = "\n".join(metadata_rows)
|
|
365
|
+
|
|
366
|
+
subtitle_html = ""
|
|
367
|
+
if subtitle:
|
|
368
|
+
subtitle_html = f'<div class="subtitle">{escape(subtitle)}</div>'
|
|
369
|
+
|
|
370
|
+
# ---------------------------------------------------------
|
|
371
|
+
# Complete document
|
|
372
|
+
# ---------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
html = f"""<!DOCTYPE html>
|
|
375
|
+
<html lang="en">
|
|
376
|
+
<head>
|
|
377
|
+
<meta charset="UTF-8">
|
|
378
|
+
<title>{escape(title)}</title>
|
|
379
|
+
<style>
|
|
380
|
+
|
|
381
|
+
* {{
|
|
382
|
+
box-sizing: border-box;
|
|
383
|
+
}}
|
|
384
|
+
|
|
385
|
+
body {{
|
|
386
|
+
margin: 0;
|
|
387
|
+
padding: 0;
|
|
388
|
+
background: #ffffff;
|
|
389
|
+
color: #111111;
|
|
390
|
+
font-family: "Times New Roman", Georgia, serif;
|
|
391
|
+
}}
|
|
392
|
+
|
|
393
|
+
.report {{
|
|
394
|
+
display: inline-block;
|
|
395
|
+
padding: 20px;
|
|
396
|
+
}}
|
|
397
|
+
|
|
398
|
+
.title {{
|
|
399
|
+
font-size: 22px;
|
|
400
|
+
font-weight: 600;
|
|
401
|
+
margin-bottom: 4px;
|
|
402
|
+
}}
|
|
403
|
+
|
|
404
|
+
.subtitle {{
|
|
405
|
+
color: #666666;
|
|
406
|
+
font-size: 13px;
|
|
407
|
+
margin-bottom: 22px;
|
|
408
|
+
overflow-wrap: break-word;
|
|
409
|
+
white-space: pre-wrap;
|
|
410
|
+
}}
|
|
411
|
+
|
|
412
|
+
/* ============================================================
|
|
413
|
+
TABLE — booktabs style: no vertical rules, rules top/bottom
|
|
414
|
+
============================================================ */
|
|
415
|
+
|
|
416
|
+
table {{
|
|
417
|
+
border-collapse: collapse;
|
|
418
|
+
font-size: 13.5px;
|
|
419
|
+
border-top: 2px solid #000000;
|
|
420
|
+
border-bottom: 2px solid #000000;
|
|
421
|
+
}}
|
|
422
|
+
|
|
423
|
+
thead th {{
|
|
424
|
+
padding: 6px 16px;
|
|
425
|
+
text-align: center;
|
|
426
|
+
font-weight: 400;
|
|
427
|
+
}}
|
|
428
|
+
|
|
429
|
+
.header-main th {{
|
|
430
|
+
font-weight: 600;
|
|
431
|
+
padding-bottom: 2px;
|
|
432
|
+
}}
|
|
433
|
+
|
|
434
|
+
.header-main .model-header {{
|
|
435
|
+
font-size: 14px;
|
|
436
|
+
}}
|
|
437
|
+
|
|
438
|
+
.model-header-inner {{
|
|
439
|
+
display: block;
|
|
440
|
+
margin: 0 6px 4px;
|
|
441
|
+
padding-bottom: 4px;
|
|
442
|
+
border-bottom: 1px solid #000000;
|
|
443
|
+
}}
|
|
444
|
+
|
|
445
|
+
.header-metric th {{
|
|
446
|
+
font-size: 12.5px;
|
|
447
|
+
font-weight: 400;
|
|
448
|
+
color: #333333;
|
|
449
|
+
border-bottom: 1px solid #000000;
|
|
450
|
+
padding-bottom: 6px;
|
|
451
|
+
}}
|
|
452
|
+
|
|
453
|
+
.corner-label {{
|
|
454
|
+
font-size: 12.5px;
|
|
455
|
+
font-weight: 400;
|
|
456
|
+
color: #000000;
|
|
457
|
+
text-transform: uppercase;
|
|
458
|
+
letter-spacing: 0.03em;
|
|
459
|
+
}}
|
|
460
|
+
|
|
461
|
+
/* ============================================================
|
|
462
|
+
BODY
|
|
463
|
+
============================================================ */
|
|
464
|
+
|
|
465
|
+
tbody th,
|
|
466
|
+
tbody td {{
|
|
467
|
+
padding: 6px 16px;
|
|
468
|
+
}}
|
|
469
|
+
|
|
470
|
+
.dataset-cell {{
|
|
471
|
+
text-align: left;
|
|
472
|
+
vertical-align: middle;
|
|
473
|
+
font-weight: 600;
|
|
474
|
+
white-space: nowrap;
|
|
475
|
+
}}
|
|
476
|
+
|
|
477
|
+
.context-cell {{
|
|
478
|
+
text-align: left;
|
|
479
|
+
padding-left: 24px !important;
|
|
480
|
+
vertical-align: middle;
|
|
481
|
+
white-space: nowrap;
|
|
482
|
+
font-style: italic;
|
|
483
|
+
color: #444444;
|
|
484
|
+
font-weight: 400;
|
|
485
|
+
}}
|
|
486
|
+
|
|
487
|
+
.value-cell {{
|
|
488
|
+
text-align: center;
|
|
489
|
+
white-space: nowrap;
|
|
490
|
+
font-variant-numeric: tabular-nums;
|
|
491
|
+
}}
|
|
492
|
+
|
|
493
|
+
tr.striped td,
|
|
494
|
+
tr.striped th {{
|
|
495
|
+
background: #f7f7f7;
|
|
496
|
+
}}
|
|
497
|
+
|
|
498
|
+
tr.dataset-start td,
|
|
499
|
+
tr.dataset-start th {{
|
|
500
|
+
border-top: 1px solid #cccccc;
|
|
501
|
+
}}
|
|
502
|
+
|
|
503
|
+
/* ============================================================
|
|
504
|
+
PER-MODEL SUMMARY STATS (time, params, FLOPs, ...)
|
|
505
|
+
============================================================ */
|
|
506
|
+
|
|
507
|
+
tbody.stats tr:first-child td,
|
|
508
|
+
tbody.stats tr:first-child th {{
|
|
509
|
+
border-top: 1px solid #000000;
|
|
510
|
+
}}
|
|
511
|
+
|
|
512
|
+
.stats-label {{
|
|
513
|
+
text-align: left;
|
|
514
|
+
font-size: 12px;
|
|
515
|
+
font-style: italic;
|
|
516
|
+
font-weight: 400;
|
|
517
|
+
color: #555555;
|
|
518
|
+
}}
|
|
519
|
+
|
|
520
|
+
.stats-value {{
|
|
521
|
+
text-align: center;
|
|
522
|
+
white-space: nowrap;
|
|
523
|
+
font-variant-numeric: tabular-nums;
|
|
524
|
+
font-size: 12.5px;
|
|
525
|
+
color: #333333;
|
|
526
|
+
}}
|
|
527
|
+
|
|
528
|
+
/* ============================================================
|
|
529
|
+
BEST RESULTS
|
|
530
|
+
============================================================ */
|
|
531
|
+
|
|
532
|
+
.best {{
|
|
533
|
+
font-weight: 700;
|
|
534
|
+
}}
|
|
535
|
+
|
|
536
|
+
/* ============================================================
|
|
537
|
+
METADATA
|
|
538
|
+
============================================================ */
|
|
539
|
+
|
|
540
|
+
.metadata {{
|
|
541
|
+
margin-top: 20px;
|
|
542
|
+
padding-top: 10px;
|
|
543
|
+
font-size: 12.5px;
|
|
544
|
+
color: #444444;
|
|
545
|
+
}}
|
|
546
|
+
|
|
547
|
+
.metadata-title {{
|
|
548
|
+
margin-bottom: 8px;
|
|
549
|
+
color: #111111;
|
|
550
|
+
font-size: 16px;
|
|
551
|
+
font-weight: 600;
|
|
552
|
+
}}
|
|
553
|
+
|
|
554
|
+
.metadata-row {{
|
|
555
|
+
display: grid;
|
|
556
|
+
grid-template-columns: 110px auto;
|
|
557
|
+
margin: 3px 0;
|
|
558
|
+
}}
|
|
559
|
+
|
|
560
|
+
.metadata-label {{
|
|
561
|
+
font-weight: 600;
|
|
562
|
+
color: #222222;
|
|
563
|
+
}}
|
|
564
|
+
|
|
565
|
+
.metadata-value {{
|
|
566
|
+
color: #000000;
|
|
567
|
+
}}
|
|
568
|
+
|
|
569
|
+
</style>
|
|
570
|
+
</head>
|
|
571
|
+
<body>
|
|
572
|
+
|
|
573
|
+
<div class="report">
|
|
574
|
+
|
|
575
|
+
<div class="title">{escape(title)}</div>
|
|
576
|
+
{subtitle_html}
|
|
577
|
+
|
|
578
|
+
<table>
|
|
579
|
+
<thead>
|
|
580
|
+
{header_row_1}
|
|
581
|
+
{header_row_2}
|
|
582
|
+
</thead>
|
|
583
|
+
<tbody>
|
|
584
|
+
{"".join(body_rows)}
|
|
585
|
+
</tbody>
|
|
586
|
+
{f'<tbody class="stats">{stats_rows_html}</tbody>' if stats_rows_html else ""}
|
|
587
|
+
</table>
|
|
588
|
+
|
|
589
|
+
<div class="metadata">
|
|
590
|
+
<div class="metadata-title">Experimental setup</div>
|
|
591
|
+
{metadata_html}
|
|
592
|
+
</div>
|
|
593
|
+
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
</body>
|
|
597
|
+
</html>
|
|
598
|
+
"""
|
|
599
|
+
|
|
600
|
+
html_path.parent.mkdir(parents=True, exist_ok=True)
|
|
601
|
+
html_path.write_text(html, encoding="utf-8")
|
|
602
|
+
|
|
603
|
+
return html
|
|
604
|
+
|
|
605
|
+
def html_to_png(self, scale: int = 2,):
|
|
606
|
+
"""Convert the .html file to the .png image."""
|
|
607
|
+
|
|
608
|
+
html_path = Path(self.html_path).resolve()
|
|
609
|
+
png_path = Path(self.png_path)
|
|
610
|
+
|
|
611
|
+
png_path.parent.mkdir(
|
|
612
|
+
parents=True,
|
|
613
|
+
exist_ok=True,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
with sync_playwright() as p:
|
|
617
|
+
browser = p.chromium.launch()
|
|
618
|
+
|
|
619
|
+
page = browser.new_page(
|
|
620
|
+
device_scale_factor=scale,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
page.goto(
|
|
624
|
+
html_path.as_uri(),
|
|
625
|
+
wait_until="networkidle",
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# Find the actual report element
|
|
629
|
+
report = page.locator(".report")
|
|
630
|
+
|
|
631
|
+
# Take a screenshot of ONLY that element
|
|
632
|
+
report.screenshot(
|
|
633
|
+
path=str(png_path),
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
browser.close()
|
|
637
|
+
|
|
638
|
+
def _get_cpu_name(self) -> str:
|
|
639
|
+
|
|
640
|
+
system = platform.system()
|
|
641
|
+
|
|
642
|
+
if system == "Windows":
|
|
643
|
+
# Uses Windows Management Instrumentation (WMI) via command line
|
|
644
|
+
try:
|
|
645
|
+
command = "wmic cpu get name"
|
|
646
|
+
output = subprocess.check_output(command, shell=True).decode().strip()
|
|
647
|
+
# The output contains a header 'Name', split and grab the actual value
|
|
648
|
+
return output.split("\n")[1].strip()
|
|
649
|
+
except Exception:
|
|
650
|
+
return platform.processor()
|
|
651
|
+
|
|
652
|
+
elif system == "Darwin": # macOS
|
|
653
|
+
# Queries the sysctl kernel utility for the CPU brand string
|
|
654
|
+
try:
|
|
655
|
+
command = ["sysctl", "-n", "machdep.cpu.brand_string"]
|
|
656
|
+
return subprocess.check_output(command).decode().strip()
|
|
657
|
+
except Exception:
|
|
658
|
+
return platform.processor()
|
|
659
|
+
|
|
660
|
+
elif system == "Linux":
|
|
661
|
+
# Reads the system's virtual /proc/cpuinfo file directly
|
|
662
|
+
try:
|
|
663
|
+
with open("/proc/cpuinfo", "r") as f:
|
|
664
|
+
for line in f:
|
|
665
|
+
if "model name" in line:
|
|
666
|
+
# Extract the name after the colon
|
|
667
|
+
return line.split(":", 1)[1].strip()
|
|
668
|
+
except Exception:
|
|
669
|
+
# Fallback if /proc/cpuinfo isn't accessible (e.g., some containers)
|
|
670
|
+
try:
|
|
671
|
+
output = subprocess.check_output("lscpu", shell=True).decode()
|
|
672
|
+
for line in output.splitlines():
|
|
673
|
+
if "Model name:" in line:
|
|
674
|
+
return line.split(":", 1)[1].strip()
|
|
675
|
+
except Exception:
|
|
676
|
+
return platform.processor()
|
|
677
|
+
|
|
678
|
+
return "Unknown Processor"
|
|
679
|
+
|
|
680
|
+
def _get_machine_info(self) -> dict:
|
|
681
|
+
"""Get the info about system and hardware, returns as dictonary."""
|
|
682
|
+
|
|
683
|
+
# OS
|
|
684
|
+
os_name = f"{platform.system()} {platform.release()}"
|
|
685
|
+
os_version = f"{platform.version()}"
|
|
686
|
+
arch = f"{platform.machine()}"
|
|
687
|
+
# CPU
|
|
688
|
+
cpu_name = f"{self._get_cpu_name()}"
|
|
689
|
+
# GPU
|
|
690
|
+
gpu_name = "_"
|
|
691
|
+
if torch.cuda.is_available():
|
|
692
|
+
gpu_name = torch.cuda.get_device_name(0)
|
|
693
|
+
# Python version
|
|
694
|
+
python_version = f"{platform.python_version()}"
|
|
695
|
+
# Pytorch version
|
|
696
|
+
pytorch_version = f"{torch.__version__}"
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
"OS": os_name + " " + os_version + " " + arch,
|
|
700
|
+
"Python": python_version,
|
|
701
|
+
"PyTorch": pytorch_version,
|
|
702
|
+
"CPU": cpu_name,
|
|
703
|
+
"GPU": gpu_name
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
def report(self, df, title, subtitle, model_stats):
|
|
707
|
+
|
|
708
|
+
# Print the report to the console #
|
|
709
|
+
|
|
710
|
+
# Title
|
|
711
|
+
console.print("=" * 128, style="white")
|
|
712
|
+
console.print(" " * 60 + "Report:", style="magenta")
|
|
713
|
+
console.print("=" * 128, style="white")
|
|
714
|
+
# DataFrame
|
|
715
|
+
console.print("-" * 128, style="white")
|
|
716
|
+
console.print(df, style="white")
|
|
717
|
+
console.print("-" * 128, style="white")
|
|
718
|
+
# Stats
|
|
719
|
+
console.print("Training stats:", style="white")
|
|
720
|
+
report_model_names = list(config.data_config)
|
|
721
|
+
console.print(f"{'Metric':<18} | " + " | ".join(f"{h:>10}" for h in report_model_names), style="white")
|
|
722
|
+
console.print("-" * 128, style="white")
|
|
723
|
+
for key, values in model_stats.items():
|
|
724
|
+
console.print(f"{key:<18} | " + " | ".join(f"{float(v):>10.2f}" for v in values), style="white")
|
|
725
|
+
console.print("-" * 128, style="white")
|
|
726
|
+
# Meta
|
|
727
|
+
console.print("Experimental setup:", style="white")
|
|
728
|
+
console.print(json.dumps(self.machine_info, indent = 4), style="white")
|
|
729
|
+
console.print("=" * 128, style="white")
|
|
730
|
+
|
|
731
|
+
# Convert and save the report #
|
|
732
|
+
|
|
733
|
+
self.dataframe_to_html(df=df, model_stats=model_stats, metadata=self.machine_info, title=title, subtitle=subtitle)
|
|
734
|
+
self.html_to_png()
|