TensorBored is a drop-in replacement for TensorBoard, focused on PyTorch workflows and an improved dashboard experience. It reads the same tfevents files, uses the same logdir structure, and provides all existing TensorBoard features plus several powerful additions.
This guide is for anyone who wants to use TensorBored — whether you are logging metrics from a training script, configuring dashboards, or building tools on top of it. You do not need to understand TensorBored's internal implementation to use this guide.
If you want to work on the TensorBored codebase itself, see AGENTS_DEV.md.
- Installation
- Quick Start
- Feature Overview
- Dashboard Profiles
- Superimposed Plots
- Pinned Card Reordering
- Metric Descriptions
- Programmatic Run Colors
- Log Scale and Symlog for Axes
- Persistent Settings
- Default Run Selection
- Migration from TensorBoard
- Complete Python API Reference
- Examples
- Storage Architecture
- Live Demo
- FAQ
pip install tensorboredTensorBored requires Python 3.9+.
TensorBored works exactly like TensorBoard. Point it at a logdir:
# These are equivalent — both commands work
tensorbored --logdir ./logs
tensorboard --logdir ./logsThen open http://localhost:6006 in your browser.
If you are already using torch.utils.tensorboard.SummaryWriter or tf.summary, your existing code works without changes. TensorBored reads the same tfevents files.
For new PyTorch projects, we recommend importing the SummaryWriter directly from TensorBored:
from tensorbored.torch import SummaryWriterThis is equivalent to from torch.utils.tensorboard import SummaryWriter but ensures the tensorboard module alias is active — no need to install the original tensorboard package separately.
To take advantage of the new features, you can optionally configure a dashboard profile from your training script:
from tensorbored.plugins.core import profile_writer
p = profile_writer.create_profile(
'My Experiment',
pinned_cards=[
profile_writer.pin_scalar('train/loss'),
profile_writer.pin_scalar('eval/accuracy'),
],
smoothing=0.8,
)
p.write('./logs')When anyone opens TensorBored pointed at this logdir, they get your pre-configured view automatically.
| Feature | What It Does |
|---|---|
| Dashboard Profiles | Save, load, export, import dashboard configurations. No more URL length limits. |
| Superimposed Plots | Overlay multiple metrics on a single chart (e.g., train/loss + eval/loss). |
| Pinned Card Reordering | Drag-and-drop to organize your pinned charts in any order. |
| Metric Descriptions | Add hover tooltips with long-form descriptions for each metric. |
| Programmatic Run Colors | Hash-based stable colors by default; full programmatic control available. |
| Color Sampler API | Generate perceptually uniform color palettes using the OKLCH color space. |
| Log/Symlog X-Axis | Log and symmetric-log scales for both X and Y axes. |
| Persistent Settings | Tag filters, run selections, pins, and section expansion survive page refreshes. |
| Default Run Selection | All runs visible by default; no more blank dashboards. |
| Pin Limit Increase | Up to 1,000 pinned cards (TensorBoard's URL-based limit was ~10-20). |
Traditional TensorBoard stores dashboard state in the URL, which hits browser URL length limits with complex configurations. TensorBored profiles solve this by storing everything in browser localStorage.
The profile menu is accessed via the bookmark icon in the top navigation bar.
| Action | Description |
|---|---|
| Save As New | Save the current dashboard state (pins, colors, filters, smoothing, etc.) as a named profile |
| Load Profile | Restore a previously saved profile |
| Export Profile | Download the active profile as a JSON file |
| Import Profile | Load a profile from a JSON file (e.g., shared by a teammate) |
| Delete Profile | Remove a saved profile |
| Deactivate Profile | Return to default state with no active profile |
| Clear All Profiles | Remove all saved profiles |
An orange dot appears on the bookmark icon when you have unsaved changes relative to the active profile.
Profiles are exported as JSON files, making them easy to share:
- Save a profile or load the default profile
- Click the bookmark icon → "Export Profile"
- Send the downloaded
.jsonfile to a teammate - They click bookmark icon → "Import Profile" → select the file
This replaces TensorBoard's approach of sharing long URLs.
The most powerful feature: configure the dashboard before users even open it.
create_profile returns a Profile object you can inspect, tweak, merge, and write to disk:
from tensorbored.plugins.core import profile_writer, color_sampler
p = profile_writer.create_profile(
'Training Monitor',
# Pin your most important metrics at the top
pinned_cards=[
profile_writer.pin_scalar('train/loss'),
profile_writer.pin_scalar('eval/loss'),
profile_writer.pin_scalar('train/accuracy'),
profile_writer.pin_scalar('eval/accuracy'),
profile_writer.pin_scalar('learning_rate'),
],
# Create comparison charts
superimposed_cards=[
profile_writer.create_superimposed_card(
title='Train vs Eval Loss',
tags=['train/loss', 'eval/loss'],
),
],
# Assign colors to runs
run_colors={
'baseline': '#9E9E9E',
'experiment_v1': '#2196F3',
'experiment_v2': '#4CAF50',
},
# Add metric descriptions (shown as hover tooltips)
metric_descriptions={
'train/loss': 'Cross-entropy loss on the training set.',
'eval/loss': 'Cross-entropy loss on the held-out validation set.',
'learning_rate': 'Effective learning rate after warmup and cosine decay.',
},
# Default filter and smoothing
tag_filter='loss|accuracy|learning_rate',
smoothing=0.8,
# Group runs by regex pattern
group_by={
'key': 'regex',
'regexString': r'(baseline|experiment)',
},
# Control which sections are expanded on load
expanded_tag_groups={
'train': True,
'eval': True,
'debug': False,
},
)
# Tweak after creation
p.pin_scalar('gradients/global_norm')
p.run_colors['experiment_v3'] = '#FF9800'
# Write to <logdir>/.tensorboard/default_profile.json
p.write('/path/to/logs')Profiles can also be merged with | or update():
base = profile_writer.create_profile(
'Base', smoothing=0.8, run_colors={'train': '#2196F3'},
)
overlay = profile_writer.create_profile(
'Overlay',
pinned_cards=[profile_writer.pin_scalar('eval/loss')],
y_axis_scale='log10',
)
combined = base | overlay # dicts merged, lists extended, scalars from overlay
combined.write(logdir)The default profile auto-applies when a user opens TensorBored at this logdir, but only if they don't already have a local profile active. User-created profiles always take priority over the backend default.
The one-shot set_default_profile(logdir, ...) helper is still available if you don't need the intermediate Profile object.
A profile contains the following fields:
| Field | Type | Description |
|---|---|---|
version |
int |
Schema version (currently 1) |
name |
str |
Human-readable profile name |
pinnedCards |
list[dict] |
Cards to pin (each has plugin, tag, optional runId, sample) |
runColors |
list[dict] |
Run color overrides (runId + hex color) |
groupColors |
list[dict] |
Group color assignments (groupKey + colorId) |
superimposedCards |
list[dict] |
Multi-tag chart definitions (id, title, tags, optional runId) |
runSelection |
list[dict] |
Which runs are visible (type, value, selected) |
metricDescriptions |
dict[str, str] |
Tag → long-form description mapping |
tagFilter |
str |
Regex to filter visible tags |
runFilter |
str |
Regex to filter visible runs |
smoothing |
float |
Scalar smoothing value (0.0–0.999) |
groupBy |
dict or null |
Run grouping config (key + optional regexString) |
expandedTagGroups |
dict[str, bool] |
Which tag group sections are expanded (true) or collapsed (false). Omit for default (first two expanded). |
Compare multiple metrics on a single chart by superimposing them. This is invaluable for comparing train vs. eval curves, or seeing how different metrics evolve together.
- Hover over any scalar card and open its menu (three dots)
- Select "Add to superimposed plot"
- Choose "Create new superimposed plot" or add to an existing one
- Superimposed cards appear in a dedicated section of the dashboard
- Each tag gets a distinct color; the title auto-updates (e.g.,
loss + accuracy) - Remove tags by clicking the X on tag chips
- If all tags are removed, the superimposed card is automatically deleted
Superimposed cards support:
- Y-axis scale cycling: Linear → Log → SymLog → Linear
- X-axis scale cycling (for Step/Relative): Linear → Log → SymLog → Linear
- Pan and zoom
- Fullscreen toggle
from tensorbored.plugins.core import profile_writer
p = profile_writer.create_profile()
p.add_superimposed_card('Train vs Eval Loss', ['loss/train', 'loss/eval'])
p.add_superimposed_card('All Accuracies', ['accuracy/train', 'accuracy/eval', 'accuracy/test'])
p.add_superimposed_card('Loss + Grad Norm', ['loss/train', 'gradients/global_norm'])
p.write('./logs')Organize your pinned cards in any order:
- Drag-and-drop: Click and hold the card header, drag to a new position
- Arrow buttons: Use the left/right arrow buttons on each card for precise positioning
- The drag handle is on the card header/control row, so dragging does not interfere with chart interactions (zoom, pan)
- Order persists across page refreshes when "Save Pinned Cards" is enabled
- Order is saved within profiles
TensorBored supports up to 1,000 pinned cards (TensorBoard's URL-based approach was limited to ~10-20 depending on tag name length).
Add long-form descriptions that appear as hover tooltips on metric card headers. This is especially useful when you have many metrics with cryptic short names.
from tensorbored.plugins.core import profile_writer
p = profile_writer.create_profile(
metric_descriptions={
'loss/train': 'Cross-entropy training loss used for backpropagation.',
'loss/eval': 'Cross-entropy loss on the held-out validation split, computed every 100 steps.',
'accuracy/train': 'Top-1 classification accuracy on the training batch.',
'accuracy/eval': 'Top-1 classification accuracy on the full validation set.',
'learning_rate': 'Effective learning rate after linear warmup (50 steps) and cosine decay.',
'gradients/global_norm': 'Global L2 norm of all model gradients before clipping.',
},
)
p.write('./logs')Descriptions are set via the metric_descriptions parameter in the profile writer. They support Markdown formatting. The descriptions are served by the backend through the /data/tags endpoint and displayed as tooltips on scalar, histogram, and image cards.
In stock TensorBoard, run colors are randomly assigned and change on every page refresh. TensorBored computes colors deterministically from the run ID hash:
- Same run always gets the same color
- Colors are stable across page refreshes and browser sessions
- No configuration needed — this is the default behavior
For full control, assign colors explicitly:
from tensorbored.plugins.core import profile_writer
profile_writer.set_default_profile(
logdir='./logs',
run_colors={
'baseline': '#9E9E9E', # Gray
'experiment_v1': '#2196F3', # Blue
'experiment_v2': '#4CAF50', # Green
'experiment_v3': '#FF9800', # Orange
},
)Colors persist in browser localStorage, so they survive refreshes and are included in exported profiles.
Generate perceptually uniform color palettes using the OKLCH color space. Equal steps in OKLCH correspond to equal perceived color differences, making colors easy to distinguish.
from tensorbored.plugins.core import color_sampler
# Generate n evenly-spaced colors
colors = color_sampler.sample_colors(5)
# ['#dc8a78', '#a4b93e', '#40c4aa', '#7aa6f5', '#d898d5']
# Auto-assign colors to a list of run IDs
run_colors = color_sampler.colors_for_runs(['train', 'eval', 'test'])
# {'train': '#dc8a78', 'eval': '#5fba72', 'test': '#7a9ef7'}
# Use the ColorMap class for index-based access
cm = color_sampler.ColorMap(5)
cm(0) # '#dc8a78'
cm(2) # '#40c4aa'
run_colors = {rid: cm(i) for i, rid in enumerate(run_ids)}
# For many runs (>8), use varied lightness/chroma for better distinction
run_colors = color_sampler.colors_for_runs(run_ids, varied=True)
# or
colors = color_sampler.sample_colors_varied(20)
# Preset palettes
categorical = color_sampler.palette_categorical(8) # High chroma, chart-optimized
sequential = color_sampler.palette_sequential(5, hue=250) # Blue, light to dark
diverging = color_sampler.palette_diverging(7) # Blue → neutral → orange
# Color utilities
lighter = color_sampler.lighten('#2196F3', 0.1)
darker = color_sampler.darken('#2196F3', 0.1)from tensorbored.plugins.core import profile_writer, color_sampler
run_names = ['baseline', 'adam_lr1e-3', 'adam_lr1e-4', 'large_batch', 'small_batch']
run_colors = color_sampler.colors_for_runs(run_names, varied=True)
profile_writer.set_default_profile(
logdir='./logs',
name='Experiment Comparison',
run_colors=run_colors,
pinned_cards=[profile_writer.pin_scalar('loss/train')],
)Open the overflow menu (three dots) on any scalar card and click the Y-axis scale option. It cycles through:
- Linear — Standard linear scale
- Log — Logarithmic (base 10) scale
- SymLog — Symmetric logarithmic scale
Available when the X-axis is set to Step or Relative (not available for Wall Time):
Open the overflow menu and click the X-axis scale option. Same cycle: Linear → Log → SymLog.
Standard log scales cannot handle zero or negative values. SymLog uses the log-modulus transformation:
symlog(x) = sign(x) * log10(|x| + 1)
This provides:
- Linear behavior near zero — smooth transition, no discontinuity
- Logarithmic behavior for large values — compresses the scale for both positive and negative extremes
- Handles negative values — unlike standard log scale
SymLog is ideal for:
- Metrics that span many orders of magnitude in both directions (e.g., gradient values)
- Loss functions that can go negative
- Any data where standard log scale would fail due to zeros or negatives
TensorBored automatically persists your dashboard customizations to browser localStorage. These survive page refreshes and browser restarts.
The regex tag filter bar remembers your last setting:
- Type
loss|accuracy→ refresh → the filter is still there - Clear the filter → refresh → it stays cleared (your explicit choice is remembered)
- If a profile has a default filter, your explicit override takes priority
Which runs are visible/hidden is persisted:
- Toggle runs on/off → refresh → your selection is preserved
- Safety net: if your saved selection would hide all runs, TensorBored resets to all-visible
Pinned cards are saved to localStorage:
- Pin cards → refresh → they are still pinned
- Pin order is preserved
- Up to 1,000 pinned cards supported
Which tag group sections are expanded or collapsed is persisted:
- Expand or collapse sections → refresh → your choice is preserved
- On first load with no persisted state, the first two sections are expanded by default
- Profiles can override this with the
expanded_tag_groupsparameter - When a profile specifies
expanded_tag_groups, those values are applied; when omitted, the default behavior (first two groups) is used
When loading the dashboard:
- All runs are visible by default (unless a profile explicitly hides some)
- If you previously hid all runs, the default is restored on refresh (prevents blank dashboards)
- Profiles can specify which runs should be visible via
runSelectionor the convenienceselected_runsparameter
TensorBored is a drop-in replacement:
| Aspect | Compatibility |
|---|---|
| Event files | Reads the same tfevents files |
| Logdir structure | Fully compatible |
| CLI | Same flags (--logdir, --port, --host, etc.) |
SummaryWriter |
Use from tensorbored.torch import SummaryWriter (recommended) or existing torch.utils.tensorboard imports |
tf.summary |
Works with TensorFlow summary writers unchanged |
| URL-based pins | Legacy URL pins from TensorBoard still work |
To switch:
pip install tensorbored
# Instead of:
tensorboard --logdir ./logs
# Use:
tensorbored --logdir ./logs
# (Both commands work — 'tensorboard' is kept as an alias)In your training scripts, replace:
# Before:
from torch.utils.tensorboard import SummaryWriter
# After (recommended):
from tensorbored.torch import SummaryWriterThe SummaryWriter API is identical — only the import changes. You no longer need the tensorboard package installed.
Module: tensorbored.plugins.core.profile_writer
The core type. Wraps the camelCase JSON the frontend expects behind snake_case properties. Created via create_profile(), Profile(...), Profile.load(logdir), or Profile.from_serialized(dict).
Properties (all have getters and setters):
| Property | Type | Default | Description |
|---|---|---|---|
name |
str |
"Default Profile" |
Display name |
pinned_cards |
list[dict] |
[] |
Cards to pin (use pin_scalar etc.) |
run_colors |
dict[str, str] |
{} |
Run name/ID → hex color |
group_colors |
dict[str, int] |
{} |
Group key → color palette index |
superimposed_cards |
list[dict] |
[] |
Multi-tag chart definitions |
run_selection |
list[dict] |
[] |
Run visibility entries |
metric_descriptions |
dict[str, str] |
{} |
Tag → description |
tag_filter |
str |
"" |
Tag filter regex |
run_filter |
str |
"" |
Run filter regex |
smoothing |
float |
0.6 |
Scalar smoothing (0.0–0.999) |
symlog_linear_threshold |
float | None |
None |
Linear threshold for symlog |
group_by |
dict | None |
None |
Run grouping config |
y_axis_scale |
str | None |
None |
"linear", "log10", or "symlog10" |
x_axis_scale |
str | None |
None |
Same (STEP/RELATIVE only) |
tag_axis_scales |
dict[str, dict] |
{} |
Per-tag axis overrides |
tag_symlog_linear_thresholds |
dict[str, float] |
{} |
Per-tag symlog thresholds |
expanded_tag_groups |
dict[str, bool] |
{} |
Section expand/collapse state |
Convenience methods:
| Method | Description |
|---|---|
pin_scalar(tag) |
Append a scalar pin |
pin_histogram(tag, run_id) |
Append a histogram pin |
pin_image(tag, run_id, sample=0) |
Append an image pin |
add_superimposed_card(title, tags, run_id=None) |
Append a superimposed card |
select_runs(run_names) |
Set visible runs by name |
Serialization & I/O:
| Method | Description |
|---|---|
serialize() -> dict |
Return SerializedProfile dict |
write(logdir) -> str |
Serialize + write to disk, returns path |
Profile.load(logdir) -> Profile | None |
Load from logdir |
Profile.from_serialized(dict) -> Profile |
Construct from dict |
Merging:
| Operation | Description |
|---|---|
a.update(b) |
Merge b into a in place (dicts merged, lists extended, scalars replaced) |
a | b |
Return a new merged profile |
a |= b |
In-place merge (same as update) |
Create a Profile object. Same parameters as set_default_profile minus logdir. This is the primary way to build profiles.
Create and write a default profile in one call. Returns the path to the written file. Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
logdir |
str |
required | TensorBoard log directory |
name |
str |
"Default Profile" |
Display name for the profile |
pinned_cards |
list[dict] |
None |
Cards to pin (use helper functions below) |
run_colors |
dict[str, str] |
None |
Mapping of run name/ID → hex color string |
group_colors |
list[dict] |
None |
Group color assignments (groupKey, colorId) |
superimposed_cards |
list[dict] |
None |
Superimposed card definitions (use create_superimposed_card) |
run_selection |
list[dict] |
None |
Explicit run visibility (type, value, selected) |
selected_runs |
list[str] |
None |
Convenience: list of run names to select (all others hidden) |
metric_descriptions |
dict[str, str] |
None |
Tag → Markdown description mapping |
tag_filter |
str |
"" |
Regex pattern to filter tags |
run_filter |
str |
"" |
Regex pattern to filter runs |
smoothing |
float |
0.6 |
Scalar smoothing (0.0–0.999) |
group_by |
dict |
None |
Grouping config (key: "RUN", "EXPERIMENT", "REGEX", or "REGEX_BY_EXP"; optional regexString) |
expanded_tag_groups |
dict[str, bool] |
None |
Which tag group sections to expand/collapse (omit for default behavior) |
Write a Profile or SerializedProfile dict to <logdir>/.tensorboard/default_profile.json.
Read the default profile from a logdir. Returns None if no profile exists or the file is invalid.
Helper to create a pinned scalar card entry.
profile_writer.pin_scalar('train/loss')
# Returns: {'plugin': 'scalars', 'tag': 'train/loss'}Helper to create a pinned histogram card entry. run_id is required for histograms.
profile_writer.pin_histogram('weights/fc1', run_id='experiment_v1')
# Returns: {'plugin': 'histograms', 'tag': 'weights/fc1', 'runId': 'experiment_v1'}Helper to create a pinned image card entry.
profile_writer.pin_image('samples/generated', run_id='experiment_v1', sample=0)
# Returns: {'plugin': 'images', 'tag': 'samples/generated', 'runId': 'experiment_v1', 'sample': 0}Helper to create a superimposed card entry combining multiple scalar tags.
profile_writer.create_superimposed_card(
title='Train vs Eval Loss',
tags=['loss/train', 'loss/eval'],
)
# Returns: {'id': 'superimposed-<timestamp>', 'title': 'Train vs Eval Loss',
# 'tags': ['loss/train', 'loss/eval'], 'runId': None}Module: tensorbored.plugins.core.color_sampler
Generate n perceptually uniform, evenly-spaced hex colors.
| Parameter | Type | Default | Description |
|---|---|---|---|
n |
int |
required | Number of colors |
lightness |
float |
0.7 |
OKLCH lightness (0–1). ~0.65 for dark backgrounds. |
chroma |
float |
0.15 |
OKLCH chroma (0–0.4). Higher = more saturated. |
hue_start |
float |
0.0 |
Starting hue angle in degrees (0–360) |
hue_range |
float |
360.0 |
Range of hues to span |
Generate n colors with varied lightness and chroma for maximum visual distinction. Recommended for >8 colors.
Callable color map. cm(i) returns the i-th color. Supports len(), iteration, and indexing.
cm = color_sampler.ColorMap(10, varied=True)
cm(0) # First color
cm(9) # Last color
cm(15) # Wraps around: same as cm(5)
list(cm) # All 10 colorsAuto-assign colors to a list of run IDs. Automatically uses varied=True when len(run_ids) > 8.
color_sampler.colors_for_runs(['train', 'eval', 'test'])
# {'train': '#dc8a78', 'eval': '#5fba72', 'test': '#7a9ef7'}Chart-optimized categorical palette (high chroma, medium lightness).
Sequential palette from light to dark at a single hue. Default is blue.
Diverging palette from one hue through neutral to another. Default: blue → neutral → orange. Works best with odd n.
Lighten a hex color by increasing its OKLCH lightness.
Darken a hex color by decreasing its OKLCH lightness.
The simplest possible usage — just point at your logdir:
pip install tensorbored
tensorbored --logdir ./my_experiment/logsA complete PyTorch training script with TensorBored integration:
import torch
import torch.nn as nn
import torch.optim as optim
from tensorbored.torch import SummaryWriter
# TensorBored extensions (optional — only needed for dashboard config)
from tensorbored.plugins.core import profile_writer, color_sampler
def setup_dashboard(logdir: str, run_names: list):
"""Configure TensorBored dashboard before training starts."""
run_colors = color_sampler.colors_for_runs(run_names, varied=True)
profile_writer.set_default_profile(
logdir=logdir,
name='Training Dashboard',
pinned_cards=[
profile_writer.pin_scalar('loss/train'),
profile_writer.pin_scalar('loss/eval'),
profile_writer.pin_scalar('accuracy/eval'),
profile_writer.pin_scalar('learning_rate'),
],
superimposed_cards=[
profile_writer.create_superimposed_card(
title='Train vs Eval Loss',
tags=['loss/train', 'loss/eval'],
),
],
run_colors=run_colors,
metric_descriptions={
'loss/train': 'Cross-entropy loss on the training batch.',
'loss/eval': 'Cross-entropy loss on the validation set (computed every epoch).',
'accuracy/eval': 'Top-1 accuracy on the validation set.',
'learning_rate': 'Current learning rate (with warmup and cosine decay).',
},
smoothing=0.8,
tag_filter='loss|accuracy|learning_rate',
)
def train(config):
model = MyModel().to(config['device'])
optimizer = optim.Adam(model.parameters(), lr=config['lr'])
criterion = nn.CrossEntropyLoss()
writer = SummaryWriter(log_dir=f"{config['logdir']}/{config['run_name']}")
for step in range(config['total_steps']):
# ... training loop ...
loss = criterion(model(inputs), targets)
loss.backward()
optimizer.step()
if step % 10 == 0:
writer.add_scalar('loss/train', loss.item(), step)
writer.add_scalar('accuracy/eval', eval_accuracy, step)
writer.add_scalar('learning_rate', optimizer.param_groups[0]['lr'], step)
if step % 100 == 0:
for name, param in model.named_parameters():
writer.add_histogram(f'weights/{name}', param, step)
writer.close()
if __name__ == '__main__':
logdir = './logs'
run_names = ['baseline', 'experiment_v1', 'experiment_v2']
# Set up dashboard once (before any training)
setup_dashboard(logdir, run_names)
# Train each run
for name in run_names:
train({'logdir': logdir, 'run_name': name, 'lr': 0.001,
'total_steps': 1000, 'device': 'cuda'})Then view results:
tensorbored --logdir ./logsConfigure a dashboard comparing multiple hyperparameter sweeps:
from tensorbored.plugins.core import profile_writer, color_sampler
# Your experiment runs
runs = {
'lr=1e-2': '#F44336', # Red — diverged
'lr=1e-3': '#2196F3', # Blue — best
'lr=1e-4': '#4CAF50', # Green — slow but steady
'lr=1e-5': '#FF9800', # Orange — too slow
}
profile_writer.set_default_profile(
logdir='./sweep_logs',
name='Learning Rate Sweep',
pinned_cards=[
profile_writer.pin_scalar('loss/train'),
profile_writer.pin_scalar('loss/eval'),
profile_writer.pin_scalar('accuracy/eval'),
],
superimposed_cards=[
profile_writer.create_superimposed_card(
title='All Training Losses',
tags=['loss/train', 'loss/eval'],
),
],
run_colors=runs,
metric_descriptions={
'loss/train': 'Training loss (cross-entropy). Watch for divergence with lr=1e-2.',
'loss/eval': 'Validation loss. Compare convergence rates across learning rates.',
'accuracy/eval': 'Validation accuracy. lr=1e-3 should reach the highest.',
},
smoothing=0.6,
tag_filter='loss|accuracy',
# Only show the best runs by default
selected_runs=['lr=1e-3', 'lr=1e-4'],
)TensorBored uses browser localStorage for client-side persistence:
| Key Pattern | Contents |
|---|---|
_tb_profile.<name> |
Saved dashboard profile data (JSON) |
_tb_profiles_index |
List of all saved profile names |
_tb_active_profile |
Name of the currently active profile |
_tb_run_selection.v1 |
Run visibility states |
_tb_run_colors.v1 |
Custom run color overrides |
_tb_tag_filter.v1 |
Tag filter regex with timestamp |
_tb_tag_group_expansion.v1 |
Section expanded/collapsed state |
_tb_card_full_width.v1 |
Cards shown at full width |
tb-saved-pins |
Pinned card list |
Server-side, the only file TensorBored writes is the default profile:
<logdir>/.tensorboard/default_profile.json
This file is read by the backend's /data/profile endpoint and served to the frontend on load.
A live demo with synthetic training data is deployed on HuggingFace Spaces:
https://demonstrandum-tensorbored-sample.hf.space
The demo includes 5 simulated training runs (baseline, adam_lr1e-3, adam_lr1e-4, large_batch, small_batch) with scalar metrics, histograms, images, and a pre-configured default profile showing all features.
Q: Do I need to change my logging code?
No. TensorBored reads standard tfevents files. Your existing SummaryWriter or tf.summary code works unchanged. The new features (profiles, colors, etc.) are optional additions. For new PyTorch projects, we recommend from tensorbored.torch import SummaryWriter — it is a drop-in replacement that removes the need to install the original tensorboard package.
Q: Where are profiles stored?
Client-side profiles are in browser localStorage. The default profile (set from Python) is at <logdir>/.tensorboard/default_profile.json.
Q: Can I use TensorBored with TensorFlow?
Yes. TensorBored is a fork of TensorBoard and supports all TensorFlow summary types. It also works with PyTorch's SummaryWriter.
Q: Do I need to install tensorboard alongside tensorbored?
No. When the real tensorboard package is not installed, tensorbored automatically registers itself under the tensorboard module name. Libraries like PyTorch that import tensorboard internally will resolve to tensorbored transparently. Use from tensorbored.torch import SummaryWriter and you only need tensorbored + torch installed.
Q: What happens if I have too many runs for distinct colors?
Use color_sampler.colors_for_runs(run_ids, varied=True) or color_sampler.sample_colors_varied(n) which vary lightness and chroma in addition to hue, providing better distinction for large numbers of runs.
Q: Can I use both tensorboard and tensorbored CLI commands?
Yes, both are registered as entry points. They run the same code.
Q: What is the maximum number of pinned cards? TensorBored supports up to 1,000 pinned cards. TensorBoard's URL-based approach was limited to roughly 10-20 depending on tag name lengths.
Q: Does the default profile overwrite my local settings? No. If you have an active local profile, the backend default profile is not applied. User-created profiles always take priority.
Q: What Python versions are supported? Python 3.9 and above.