Skip to content

Commit 0b63429

Browse files
feat: add learning curve method (#1568)
* implement learning curve * add learning curve to integration to verify functionality * edit Mapping from tensor to any * Updated learning_curve to target each metric by name instead of relying on column position --------- Co-authored-by: Mario Behling <mb@mariobehling.de>
1 parent 3adfd6e commit 0b63429

6 files changed

Lines changed: 170 additions & 0 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ The following API is currently supported:
285285
- [`vis.scatter`](#visscatter) : 2D or 3D scatter plots
286286
- [`vis.sunburst`](#vissunburst) : sunburst (hierarchy) charts
287287
- [`vis.line`](#visline) : line plots
288+
- [`vis.learning_curve`](#vislearning_curve) : named training metric curves
288289
- [`vis.stem`](#visstem) : stem plots
289290
- [`vis.heatmap`](#visheatmap) : heatmap plots
290291
- [`vis.confusion_matrix`](#visconfusion_matrix) : confusion matrix plots
@@ -581,6 +582,29 @@ The following `opts` are supported:
581582
- `opts.layoutopts` : additional backend layout options (`dict`)
582583

583584

585+
#### vis.learning_curve
586+
This function draws named machine-learning metrics as line plots. It accepts a mapping from metric names to scalar values or equal-length 1D series and forwards to [`vis.line`](#visline).
587+
588+
For example:
589+
590+
```python
591+
win = vis.learning_curve(
592+
{"train_loss": [1.0, 0.8, 0.6], "val_loss": [1.1, 0.9, 0.7]},
593+
step=[1, 2, 3],
594+
env="training",
595+
opts={"title": "Loss", "ylabel": "loss"},
596+
)
597+
598+
vis.learning_curve(
599+
{"train_loss": 0.55, "val_loss": 0.68},
600+
step=4,
601+
win=win,
602+
env="training",
603+
update="append",
604+
)
605+
```
606+
607+
584608
#### vis.stem
585609
This function draws a stem plot. It takes as input an `N` or `NxM` tensor
586610
`X` that specifies the values of the `N` points in the `M` time series.

cypress/integration/pane.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const basic_examples = [
1717
// ["Mesh Plot", "plot_special_mesh"], // disabled due to webgl
1818
['Graph Plot', 'plot_special_graph'],
1919
['Sankey Plot', 'plot_special_sankey'],
20+
['Learning Curve', 'plot_line_learning_curve'],
2021
['Matplotlib Plot', 'misc_plot_matplot'],
2122
['Latex Plot', 'misc_plot_latex'],
2223
['Video Pane', 'misc_video_tensor'],

example/components/plot_line.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,33 @@ def plot_line_many_updates(viz, env, args):
114114
)
115115

116116

117+
def plot_line_learning_curve(viz, env, args):
118+
epochs = np.arange(1, 11)
119+
train_loss = np.exp(-epochs / 4.0) + 0.08 * np.random.rand(len(epochs))
120+
val_loss = np.exp(-epochs / 3.7) + 0.12 * np.random.rand(len(epochs)) + 0.08
121+
win = viz.learning_curve(
122+
{
123+
"train_loss": train_loss,
124+
"val_loss": val_loss,
125+
},
126+
step=epochs,
127+
opts={"title": "Learning curve", "ylabel": "loss"},
128+
env=env,
129+
)
130+
131+
for epoch in range(11, 16):
132+
viz.learning_curve(
133+
{
134+
"train_loss": np.exp(-epoch / 4.0),
135+
"val_loss": np.exp(-epoch / 3.7) + 0.08,
136+
},
137+
step=epoch,
138+
win=win,
139+
update="append",
140+
env=env,
141+
)
142+
143+
117144
def plot_line_opts(viz, env, args):
118145
return viz.line(
119146
X=np.column_stack(

example/demo.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
plot_line_pytorch,
7474
plot_line_stem,
7575
plot_line_many_updates,
76+
plot_line_learning_curve,
7677
)
7778
from components.plot_special import (
7879
plot_special_boxplot,
@@ -149,6 +150,7 @@ def run_demo(viz, env, args):
149150
plot_line_doubleyaxis(viz, env, args)
150151
plot_line_pytorch(viz, env, args)
151152
plot_line_stem(viz, env, args)
153+
plot_line_learning_curve(viz, env, args)
152154

153155
# ============= #
154156
# scatter plots #

py/visdom/__init__.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2034,6 +2034,113 @@ def update_window_opts(self, win, opts, env=None):
20342034
}
20352035
return self._send(data_to_send, endpoint="update")
20362036

2037+
@pytorch_wrap
2038+
def learning_curve(
2039+
self,
2040+
metrics,
2041+
step=None,
2042+
win=None,
2043+
env=None,
2044+
opts=None,
2045+
update=None,
2046+
):
2047+
"""
2048+
This function draws machine-learning learning curves using named metrics.
2049+
It is a convenience wrapper around `line`, accepting a mapping from metric
2050+
names to scalar values or equal-length 1D series.
2051+
2052+
`metrics` should be a non-empty mapping, for example:
2053+
2054+
{"train_loss": [1.0, 0.8], "val_loss": [1.1, 0.9]}
2055+
2056+
`step` can be a scalar or a 1D series matching the metric length. When
2057+
`update='append'`, `step` must be specified to avoid repeatedly appending
2058+
points at the same x-coordinate.
2059+
"""
2060+
assert hasattr(metrics, "items"), "metrics should be a mapping"
2061+
metric_items = list(metrics.items())
2062+
assert len(metric_items) > 0, "must provide at least one metric"
2063+
2064+
names = []
2065+
series = []
2066+
for metric_name, metric_values in metric_items:
2067+
name = str(metric_name)
2068+
values = np.asarray(_to_numpy(metric_values))
2069+
values = np.squeeze(values)
2070+
if values.ndim == 0:
2071+
values = values.reshape(1)
2072+
assert (
2073+
values.ndim == 1
2074+
), "metric '{}' should be a scalar or one-dimensional".format(name)
2075+
assert values.shape[0] > 0, "metric '{}' should not be empty".format(name)
2076+
names.append(name)
2077+
series.append(values)
2078+
2079+
num_steps = series[0].shape[0]
2080+
for name, values in zip(names, series):
2081+
assert (
2082+
values.shape[0] == num_steps
2083+
), "metric '{}' has length {}, expected {}".format(
2084+
name, values.shape[0], num_steps
2085+
)
2086+
2087+
if step is None:
2088+
assert update != "append", "must specify step when update='append'"
2089+
X = np.arange(num_steps)
2090+
else:
2091+
X = np.asarray(_to_numpy(step))
2092+
X = np.squeeze(X)
2093+
if X.ndim == 0:
2094+
X = X.reshape(1)
2095+
assert X.ndim == 1, "step should be a scalar or one-dimensional"
2096+
assert X.shape[0] == num_steps, "step has length {}, expected {}".format(
2097+
X.shape[0], num_steps
2098+
)
2099+
2100+
opts = {} if opts is None else dict(opts)
2101+
opts.setdefault("title", "Learning Curve")
2102+
opts.setdefault("xlabel", "step")
2103+
opts.setdefault("ylabel", "metric")
2104+
if opts.get("legend") is None:
2105+
legend = names
2106+
opts["legend"] = legend
2107+
else:
2108+
legend = opts["legend"]
2109+
assert isinstance(legend, (tuple, list)), "legend should be a list or tuple"
2110+
assert len(legend) >= len(
2111+
names
2112+
), "legend should have at least as many entries as metrics"
2113+
2114+
if update is not None:
2115+
result = None
2116+
# Send one named update per metric so mapping order cannot swap traces.
2117+
for name, values in zip(names, series):
2118+
metric_opts = None
2119+
if update != "remove":
2120+
metric_opts = dict(opts)
2121+
metric_opts["legend"] = [name]
2122+
result = self.line(
2123+
X=X,
2124+
Y=values,
2125+
win=win,
2126+
env=env,
2127+
opts=metric_opts,
2128+
update=update,
2129+
name=name,
2130+
)
2131+
return result
2132+
2133+
Y = np.column_stack(series)
2134+
2135+
return self.line(
2136+
X=X,
2137+
Y=Y,
2138+
win=win,
2139+
env=env,
2140+
opts=opts,
2141+
update=update,
2142+
)
2143+
20372144
@pytorch_wrap
20382145
def scatter(self, X, Y=None, win=None, env=None, opts=None, update=None, name=None):
20392146
"""

py/visdom/__init__.pyi

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ class Visdom:
127127
def update_window_opts(
128128
self, win: Text, opts: Mapping[Text, Any], env: _OptStr = ...
129129
) -> _SendReturn: ...
130+
def learning_curve(
131+
self,
132+
metrics: Mapping[Text, Any],
133+
step: Optional[Tensor] = ...,
134+
win: _OptStr = ...,
135+
env: _OptStr = ...,
136+
opts: _OptOps = ...,
137+
update: _OptStr = ...,
138+
) -> _SendReturn: ...
130139
def scatter(
131140
self,
132141
X: Tensor,

0 commit comments

Comments
 (0)