@@ -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 """
0 commit comments