Safemotion Lib
Loading...
Searching...
No Matches
events.py
Go to the documentation of this file.
1# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
2import datetime
3import json
4import logging
5import os
6import time
7from collections import defaultdict
8from contextlib import contextmanager
9import torch
10from .file_io import PathManager
11from .history_buffer import HistoryBuffer
12
13__all__ = [
14 "get_event_storage",
15 "JSONWriter",
16 "TensorboardXWriter",
17 "CommonMetricPrinter",
18 "EventStorage",
19]
20
21_CURRENT_STORAGE_STACK = []
22
23
25 """
26 Returns:
27 The :class:`EventStorage` object that's currently being used.
28 Throws an error if no :class:`EventStorage` is currently enabled.
29 """
30 assert len(
31 _CURRENT_STORAGE_STACK
32 ), "get_event_storage() has to be called inside a 'with EventStorage(...)' context!"
33 return _CURRENT_STORAGE_STACK[-1]
34
35
37 """
38 Base class for writers that obtain events from :class:`EventStorage` and process them.
39 """
40
41 def write(self):
42 raise NotImplementedError
43
44 def close(self):
45 pass
46
47
48class JSONWriter(EventWriter):
49 """
50 Write scalars to a json file.
51 It saves scalars as one json per line (instead of a big json) for easy parsing.
52 Examples parsing such a json file:
53 ::
54 $ cat metrics.json | jq -s '.[0:2]'
55 [
56 {
57 "data_time": 0.008433341979980469,
58 "iteration": 20,
59 "loss": 1.9228371381759644,
60 "loss_box_reg": 0.050025828182697296,
61 "loss_classifier": 0.5316952466964722,
62 "loss_mask": 0.7236229181289673,
63 "loss_rpn_box": 0.0856662318110466,
64 "loss_rpn_cls": 0.48198649287223816,
65 "lr": 0.007173333333333333,
66 "time": 0.25401854515075684
67 },
68 {
69 "data_time": 0.007216215133666992,
70 "iteration": 40,
71 "loss": 1.282649278640747,
72 "loss_box_reg": 0.06222952902317047,
73 "loss_classifier": 0.30682939291000366,
74 "loss_mask": 0.6970193982124329,
75 "loss_rpn_box": 0.038663312792778015,
76 "loss_rpn_cls": 0.1471673548221588,
77 "lr": 0.007706666666666667,
78 "time": 0.2490077018737793
79 }
80 ]
81 $ cat metrics.json | jq '.loss_mask'
82 0.7126231789588928
83 0.689423680305481
84 0.6776131987571716
85 ...
86 """
87
88 def __init__(self, json_file, window_size=20):
89 """
90 Args:
91 json_file (str): path to the json file. New data will be appended if the file exists.
92 window_size (int): the window size of median smoothing for the scalars whose
93 `smoothing_hint` are True.
94 """
95 self._file_handle = PathManager.open(json_file, "a")
96 self._window_size = window_size
97 self._last_write = -1
98
99 def write(self):
100 storage = get_event_storage()
101 to_save = defaultdict(dict)
102
103 for k, (v, iter) in storage.latest_with_smoothing_hint(self._window_size).items():
104 # keep scalars that have not been written
105 if iter <= self._last_write:
106 continue
107 to_save[iter][k] = v
108 all_iters = sorted(to_save.keys())
109 self._last_write = max(all_iters)
110
111 for itr, scalars_per_iter in to_save.items():
112 scalars_per_iter["iteration"] = itr
113 self._file_handle.write(json.dumps(scalars_per_iter, sort_keys=True) + "\n")
114 self._file_handle.flush()
115 try:
116 os.fsync(self._file_handle.fileno())
117 except AttributeError:
118 pass
119
120 def close(self):
121 self._file_handle.close()
122
123
125 """
126 Write all scalars to a tensorboard file.
127 """
128
129 def __init__(self, log_dir: str, window_size: int = 20, **kwargs):
130 """
131 Args:
132 log_dir (str): the directory to save the output events
133 window_size (int): the scalars will be median-smoothed by this window size
134 kwargs: other arguments passed to `torch.utils.tensorboard.SummaryWriter(...)`
135 """
136 self._window_size = window_size
137 from torch.utils.tensorboard import SummaryWriter
138
139 self._writer = SummaryWriter(log_dir, **kwargs)
140 self._last_write = -1
141
142 def write(self):
143 storage = get_event_storage()
144 new_last_write = self._last_write
145 for k, (v, iter) in storage.latest_with_smoothing_hint(self._window_size).items():
146 if iter > self._last_write:
147 self._writer.add_scalar(k, v, iter)
148 new_last_write = max(new_last_write, iter)
149 self._last_write = new_last_write
150
151 # storage.put_{image,histogram} is only meant to be used by
152 # tensorboard writer. So we access its internal fields directly from here.
153 if len(storage._vis_data) >= 1:
154 for img_name, img, step_num in storage._vis_data:
155 self._writer.add_image(img_name, img, step_num)
156 # Storage stores all image data and rely on this writer to clear them.
157 # As a result it assumes only one writer will use its image data.
158 # An alternative design is to let storage store limited recent
159 # data (e.g. only the most recent image) that all writers can access.
160 # In that case a writer may not see all image data if its period is long.
161 storage.clear_images()
162
163 if len(storage._histograms) >= 1:
164 for params in storage._histograms:
165 self._writer.add_histogram_raw(**params)
166 storage.clear_histograms()
167
168 def close(self):
169 if hasattr(self, "_writer"): # doesn't exist when the code fails at import
170 self._writer.close()
171
172
174 """
175 Print **common** metrics to the terminal, including
176 iteration time, ETA, memory, all losses, and the learning rate.
177 It also applies smoothing using a window of 20 elements.
178 It's meant to print common metrics in common ways.
179 To print something in more customized ways, please implement a similar printer by yourself.
180 """
181
182 def __init__(self, max_iter):
183 """
184 Args:
185 max_iter (int): the maximum number of iterations to train.
186 Used to compute ETA.
187 """
188 self.logger = logging.getLogger(__name__)
189 self._max_iter = max_iter
190 self._last_write = None
191
192 def write(self):
193 storage = get_event_storage()
194 iteration = storage.iter
195
196 try:
197 data_time = storage.history("data_time").avg(20)
198 except KeyError:
199 # they may not exist in the first few iterations (due to warmup)
200 # or when SimpleTrainer is not used
201 data_time = None
202
203 eta_string = None
204 try:
205 iter_time = storage.history("time").global_avg()
206 eta_seconds = storage.history("time").median(1000) * (self._max_iter - iteration)
207 storage.put_scalar("eta_seconds", eta_seconds, smoothing_hint=False)
208 eta_string = str(datetime.timedelta(seconds=int(eta_seconds)))
209 except KeyError:
210 iter_time = None
211 # estimate eta on our own - more noisy
212 if self._last_write is not None:
213 estimate_iter_time = (time.perf_counter() - self._last_write[1]) / (
214 iteration - self._last_write[0]
215 )
216 eta_seconds = estimate_iter_time * (self._max_iter - iteration)
217 eta_string = str(datetime.timedelta(seconds=int(eta_seconds)))
218 self._last_write = (iteration, time.perf_counter())
219
220 try:
221 lr = "{:.2e}".format(storage.history("lr").latest())
222 except KeyError:
223 lr = "N/A"
224
225 if torch.cuda.is_available():
226 max_mem_mb = torch.cuda.max_memory_allocated() / 1024.0 / 1024.0
227 else:
228 max_mem_mb = None
229
230 # NOTE: max_mem is parsed by grep in "dev/parse_results.sh"
231 self.logger.info(
232 " {eta}iter: {iter} {losses} {time}{data_time}lr: {lr} {memory}".format(
233 eta=f"eta: {eta_string} " if eta_string else "",
234 iter=iteration,
235 losses=" ".join(
236 [
237 "{}: {:.4g}".format(k, v.median(20))
238 for k, v in storage.histories().items()
239 if "loss" in k
240 ]
241 ),
242 time="time: {:.4f} ".format(iter_time) if iter_time is not None else "",
243 data_time="data_time: {:.4f} ".format(data_time) if data_time is not None else "",
244 lr=lr,
245 memory="max_mem: {:.0f}M".format(max_mem_mb) if max_mem_mb is not None else "",
246 )
247 )
248
249
251 """
252 The user-facing class that provides metric storage functionalities.
253 In the future we may add support for storing / logging other types of data if needed.
254 """
255
256 def __init__(self, start_iter=0):
257 """
258 Args:
259 start_iter (int): the iteration number to start with
260 """
261 self._history = defaultdict(HistoryBuffer)
264 self._iter = start_iter
266 self._vis_data = []
267 self._histograms = []
268
269 def put_image(self, img_name, img_tensor):
270 """
271 Add an `img_tensor` associated with `img_name`, to be shown on
272 tensorboard.
273 Args:
274 img_name (str): The name of the image to put into tensorboard.
275 img_tensor (torch.Tensor or numpy.array): An `uint8` or `float`
276 Tensor of shape `[channel, height, width]` where `channel` is
277 3. The image format should be RGB. The elements in img_tensor
278 can either have values in [0, 1] (float32) or [0, 255] (uint8).
279 The `img_tensor` will be visualized in tensorboard.
280 """
281 self._vis_data.append((img_name, img_tensor, self._iter))
282
283 def put_scalar(self, name, value, smoothing_hint=True):
284 """
285 Add a scalar `value` to the `HistoryBuffer` associated with `name`.
286 Args:
287 smoothing_hint (bool): a 'hint' on whether this scalar is noisy and should be
288 smoothed when logged. The hint will be accessible through
289 :meth:`EventStorage.smoothing_hints`. A writer may ignore the hint
290 and apply custom smoothing rule.
291 It defaults to True because most scalars we save need to be smoothed to
292 provide any useful signal.
293 """
294 name = self._current_prefix + name
295 history = self._history[name]
296 value = float(value)
297 history.update(value, self._iter)
298 self._latest_scalars[name] = (value, self._iter)
299
300 existing_hint = self._smoothing_hints.get(name)
301 if existing_hint is not None:
302 assert (
303 existing_hint == smoothing_hint
304 ), "Scalar {} was put with a different smoothing_hint!".format(name)
305 else:
306 self._smoothing_hints[name] = smoothing_hint
307
308 def put_scalars(self, *, smoothing_hint=True, **kwargs):
309 """
310 Put multiple scalars from keyword arguments.
311 Examples:
312 storage.put_scalars(loss=my_loss, accuracy=my_accuracy, smoothing_hint=True)
313 """
314 for k, v in kwargs.items():
315 self.put_scalar(k, v, smoothing_hint=smoothing_hint)
316
317 def put_histogram(self, hist_name, hist_tensor, bins=1000):
318 """
319 Create a histogram from a tensor.
320 Args:
321 hist_name (str): The name of the histogram to put into tensorboard.
322 hist_tensor (torch.Tensor): A Tensor of arbitrary shape to be converted
323 into a histogram.
324 bins (int): Number of histogram bins.
325 """
326 ht_min, ht_max = hist_tensor.min().item(), hist_tensor.max().item()
327
328 # Create a histogram with PyTorch
329 hist_counts = torch.histc(hist_tensor, bins=bins)
330 hist_edges = torch.linspace(start=ht_min, end=ht_max, steps=bins + 1, dtype=torch.float32)
331
332 # Parameter for the add_histogram_raw function of SummaryWriter
333 hist_params = dict(
334 tag=hist_name,
335 min=ht_min,
336 max=ht_max,
337 num=len(hist_tensor),
338 sum=float(hist_tensor.sum()),
339 sum_squares=float(torch.sum(hist_tensor ** 2)),
340 bucket_limits=hist_edges[1:].tolist(),
341 bucket_counts=hist_counts.tolist(),
342 global_step=self._iter,
343 )
344 self._histograms.append(hist_params)
345
346 def history(self, name):
347 """
348 Returns:
349 HistoryBuffer: the scalar history for name
350 """
351 ret = self._history.get(name, None)
352 if ret is None:
353 raise KeyError("No history metric available for {}!".format(name))
354 return ret
355
356 def histories(self):
357 """
358 Returns:
359 dict[name -> HistoryBuffer]: the HistoryBuffer for all scalars
360 """
361 return self._history
362
363 def latest(self):
364 """
365 Returns:
366 dict[str -> (float, int)]: mapping from the name of each scalar to the most
367 recent value and the iteration number its added.
368 """
369 return self._latest_scalars
370
371 def latest_with_smoothing_hint(self, window_size=20):
372 """
373 Similar to :meth:`latest`, but the returned values
374 are either the un-smoothed original latest value,
375 or a median of the given window_size,
376 depend on whether the smoothing_hint is True.
377 This provides a default behavior that other writers can use.
378 """
379 result = {}
380 for k, (v, itr) in self._latest_scalars.items():
381 result[k] = (
382 self._history[k].median(window_size) if self._smoothing_hints[k] else v,
383 itr,
384 )
385 return result
386
388 """
389 Returns:
390 dict[name -> bool]: the user-provided hint on whether the scalar
391 is noisy and needs smoothing.
392 """
393 return self._smoothing_hints
394
395 def step(self):
396 """
397 User should call this function at the beginning of each iteration, to
398 notify the storage of the start of a new iteration.
399 The storage will then be able to associate the new data with the
400 correct iteration number.
401 """
402 self._iter += 1
403
404 @property
405 def iter(self):
406 return self._iter
407
408 @property
409 def iteration(self):
410 # for backward compatibility
411 return self._iter
412
413 def __enter__(self):
414 _CURRENT_STORAGE_STACK.append(self)
415 return self
416
417 def __exit__(self, exc_type, exc_val, exc_tb):
418 assert _CURRENT_STORAGE_STACK[-1] == self
419 _CURRENT_STORAGE_STACK.pop()
420
421 @contextmanager
422 def name_scope(self, name):
423 """
424 Yields:
425 A context within which all the events added to this storage
426 will be prefixed by the name scope.
427 """
428 old_prefix = self._current_prefix
429 self._current_prefix = name.rstrip("/") + "/"
430 yield
431 self._current_prefix = old_prefix
432
433 def clear_images(self):
434 """
435 Delete all the stored images for visualization. This should be called
436 after images are written to tensorboard.
437 """
438 self._vis_data = []
439
441 """
442 Delete all the stored histograms for visualization.
443 This should be called after histograms are written to tensorboard.
444 """
445 self._histograms = []
put_scalars(self, *smoothing_hint=True, **kwargs)
Definition events.py:308
put_histogram(self, hist_name, hist_tensor, bins=1000)
Definition events.py:317
__exit__(self, exc_type, exc_val, exc_tb)
Definition events.py:417
__init__(self, start_iter=0)
Definition events.py:256
put_scalar(self, name, value, smoothing_hint=True)
Definition events.py:283
put_image(self, img_name, img_tensor)
Definition events.py:269
latest_with_smoothing_hint(self, window_size=20)
Definition events.py:371
__init__(self, json_file, window_size=20)
Definition events.py:88
__init__(self, str log_dir, int window_size=20, **kwargs)
Definition events.py:129