TensorBoard

Mencatat eksperimen ke TensorBoard dan membacanya secara visual.

Daftar isi

Topik ini membahas pencatatan eksperimen dengan TensorBoard, yaitu alat yang menampilkan angka-angka pelatihan sebagai kurva yang dapat dibaca secara visual. Nilai loss dan learning rate yang selama ini hanya berupa deretan cetakan berubah menjadi grafik yang memperlihatkan bentuk konvergensinya. Kelas SummaryWriter dari PyTorch digunakan untuk menulis file catatan, kemudian dasbor TensorBoard digunakan untuk membacanya kembali. Topik juga menunjukkan cara membaca file catatan tanpa server, cara mengatur penamaan agar beberapa eksperimen dapat disandingkan, serta demo interaktif yang meniru perilaku dasbornya.

SummaryWriter dan event file#

SummaryWriter menulis catatan ke sebuah event file, yaitu file biner bawaan TensorBoard, di dalam folder yang menamai eksperimen. Metode add_scalar mencatat satu nilai dengan tiga argumen, yaitu tag sebagai nama kurva, nilai skalanya, dan global_step yang menjadi posisi kurva pada sumbu mendatar. Konvensi folder runs digunakan secara luas karena perintah pembuka dasbor TensorBoard cukup menunjuk ke folder itu saja. Skrip berikut melatih regresi linear kecil dan mencatat loss serta learning rate pada setiap epoch:

python catat-scalar.py
import torch
import torch.nn as nn
from torch.utils.tensorboard import SummaryWriter

torch.manual_seed(0)

# regresi linear kecil: y = 2x + 1 dengan sedikit derau
x = torch.linspace(0.0, 1.0, 64).unsqueeze(1)   # shape (64, 1)
y = 2.0 * x + 1.0 + 0.05 * torch.randn(64, 1)   # shape (64, 1)

model = nn.Linear(1, 1)
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=2, gamma=0.5)

writer = SummaryWriter("runs/latihan-pertama")
print("Event file ditulis ke :", writer.log_dir)

for epoch in range(5):
    optimizer.zero_grad()
    y_pred = model(x)                     # shape (64, 1)
    loss = criterion(y_pred, y)
    loss.backward()
    optimizer.step()
    scheduler.step()
    writer.add_scalar("pelatihan/loss", loss.item(), epoch + 1)
    writer.add_scalar("pelatihan/lr", optimizer.param_groups[0]["lr"], epoch + 1)
    print(f"epoch {epoch + 1}: loss {loss.item():.4f}")

writer.flush()
writer.close()
print("Dua tag dicatat: pelatihan/loss dan pelatihan/lr")
text Keluaran
Event file ditulis ke : runs/latihan-pertama
epoch 1: loss 3.6374
epoch 2: loss 0.2968
epoch 3: loss 0.0522
epoch 4: loss 0.0341
epoch 5: loss 0.0300
Dua tag dicatat: pelatihan/loss dan pelatihan/lr

Properti log_dir menyebutkan folder tempat catatan ditulis, dan di dalamnya terdapat satu file yang namanya diawali events.out.tfevents diikuti waktu penulisan. Dua tag berbeda menghasilkan dua kurva terpisah pada dasbor, sedangkan global_step yang diisi nomor epoch menjadikan sumbu mendatarnya berupa epoch. Metode flush() memaksa penulisan segera, dan close() menutup file dengan rapi pada akhir pelatihan.

Membaca event file tanpa server#

Perintah tensorboard --inspect membaca kembali isi event file dan melaporkan tag serta statistiknya tanpa memulai server. Pemeriksaan semacam ini berguna pada server tanpa antarmuka grafis, pada pemecahan masalah saat dasbor tampak kosong, dan sebagai cara cepat memastikan catatan tidak kosong sebelum filenya diunduh. Perintah berikut memeriksa folder yang baru saja ditulis:

bash
# membaca ulang isi event file tanpa memulai server, lalu buang pesan stderr
# tentang ketiadaan TensorFlow karena tidak relevan di sini
tensorboard --inspect --logdir runs/latihan-pertama 2>/dev/null
text Keluaran
======================================================================
Processing event files... (this can take a few minutes)
======================================================================

Found event files in:
runs/latihan-pertama

These tags are in runs/latihan-pertama:
audio -
histograms -
images -
scalars
   pelatihan/loss
   pelatihan/lr
tensor -
======================================================================

Event statistics for runs/latihan-pertama:
audio -
graph -
histograms -
images -
scalars
   first_step           1
   last_step            5
   max_step             5
   min_step             1
   num_steps            5
   outoforder_steps     []
sessionlog:checkpoint -
sessionlog:start -
sessionlog:stop -
tensor -
======================================================================

Bagian These tags memastikan kedua tag tertulis, sedangkan bagian statistik menyatakan langkah yang tercatat berkisar dari 1 sampai 5 tanpa langkah yang tak berurutan. Kategori lain yang tampil dengan tanda hubung, misalnya images dan histograms, berarti tidak ada satu pun nilai dengan jenis itu pada file tersebut. Apabila folder yang diperiksa memuat beberapa event file dari beberapa run, seluruh tag dari semua run dilaporkan sekaligus pada laporan yang sama.

Dasbor TensorBoard#

Dasbor TensorBoard berjalan sebagai server web lokal yang membaca folder catatan dan menyajikannya di browser:

bash
# tidak-dijalankan
# perintah ini memulai server TensorBoard yang berjalan terus menerima permintaan,
# sehingga tidak dapat dieksekusi di sandbox
tensorboard --logdir runs

Server melayani alamat http://localhost:6006, dan dasbor SCALARS menampilkan setiap tag sebagai satu kurva dengan sumbu mendatar berupa global_step. Penggeser smoothing meredam goyangan kurva untuk membantu membaca kecenderungannya, sedangkan kurva mentah tetap tersedia di belakangnya. Setiap subfolder di dalam runs dihitung sebagai satu run, dan kurva dari beberapa run dengan tag yang sama ditampilkan berdampingan sehingga perbandingan eksperimen terjadi secara alami.

Pembacaan kurvanya mengikuti beberapa kebiasaan yang lazim. Loss pelatihan yang turun cepat lalu mendatar menandakan pelatihan mulai mencapai batas kemampuannya, sedangkan kurva yang masih menurun di akhir pelatihan menandakan jumlah epoch masih kurang. Kurva yang bergerak naik-turun ekstrem biasanya menunjukkan learning rate yang terlalu besar, dan gejala itu membaik dengan nilai yang lebih kecil atau dengan jadwal penurunannya.

Demo berikut meniru perilaku dasbor tersebut, yaitu dua kurva loss dari dua eksperimen berbeda beserta penggeser smoothing. Geser penggeser tersebut dan amati kurva yang meredam tanpa kehilangan kecenderungannya.

Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Simulasi dasbor scalar TensorBoard</title>
  <style>
    body { font-family: sans-serif; margin: 12px; color: #263238; }
    h3 { margin: 0 0 4px; font-size: 14px; }
    p.catatan { margin: 2px 0 10px; font-size: 12px; color: #546e7a; }
    #panel { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; font-size: 12px; }
    #smoothing { width: 200px; }
    #nilai-smoothing { display: inline-block; min-width: 44px; font-family: monospace; }
    .legenda { display: inline-flex; align-items: center; gap: 5px; margin-right: 14px; font-size: 12px; }
    .swatch { width: 14px; height: 4px; display: inline-block; }
  </style>
</head>
<body>
  <h3>Kurva loss dua eksperimen</h3>
  <p class="catatan">Geser smoothing untuk meredam goyangan kurva, persis seperti dasbor SCALARS TensorBoard.</p>
  <div id="panel">
    <label for="smoothing">Smoothing</label>
    <input id="smoothing" type="range" min="0" max="0.95" step="0.05" value="0">
    <span id="nilai-smoothing">0.00</span>
    <span class="legenda"><span class="swatch" style="background:#3949ab"></span>run-a</span>
    <span class="legenda"><span class="swatch" style="background:#c62828"></span>run-b</span>
  </div>
  <svg id="grafik" width="100%" height="240" viewBox="0 0 560 240" role="img" aria-label="Grafik loss dua eksperimen dengan penggeser smoothing"></svg>
  <script src="index.js"></script>

<script>
// Simulasi sederhana dasbor SCALARS TensorBoard: kurva loss dua run
// dengan penggeser smoothing (rata-rata bergerak eksponensial).

const runA = [1.2950, 1.1162, 1.0401, 0.9686, 0.8502, 0.8086, 0.7459, 0.6278, 0.6735, 0.6125, 0.5276, 0.5054, 0.4947, 0.4368, 0.4093, 0.3414, 0.3886, 0.3527, 0.3391, 0.2590, 0.3547, 0.2884, 0.2568, 0.3299, 0.2469, 0.1884, 0.2165, 0.1429, 0.2528, 0.1952];
const runB = [1.3185, 1.3113, 1.1496, 1.1640, 1.0148, 1.0093, 0.9392, 0.9846, 0.9503, 0.8349, 0.8365, 0.7638, 0.7554, 0.6767, 0.6128, 0.5810, 0.6309, 0.6711, 0.5784, 0.5287, 0.5934, 0.5155, 0.4926, 0.4810, 0.4517, 0.4306, 0.3773, 0.4320, 0.3989, 0.4325];

const WARNA = { a: "#3949ab", b: "#c62828" };
const M = { kiri: 46, kanan: 12, atas: 12, bawah: 30 };
const LEBAR = 560, TINGGI = 240;
const YMAKS = 1.4;
const svg = document.getElementById("grafik");

function xTitik(i, n) {
  return M.kiri + i / (n - 1) * (LEBAR - M.kiri - M.kanan);
}

function yTitik(nilai) {
  return M.atas + (1 - nilai / YMAKS) * (TINGGI - M.atas - M.bawah);
}

function elemen(nama, atribut) {
  const el = document.createElementNS("http://www.w3.org/2000/svg", nama);
  for (const k in atribut) el.setAttribute(k, atribut[k]);
  svg.appendChild(el);
  return el;
}

function garisData(data, warna, tebal, opasitas) {
  const titik = data.map((v, i) => xTitik(i, data.length).toFixed(1) + "," + yTitik(v).toFixed(1)).join(" ");
  elemen("polyline", { points: titik, fill: "none", stroke: warna, "stroke-width": tebal, opacity: opasitas });
}

function ema(data, a) {
  const hasil = [data[0]];
  for (let i = 1; i < data.length; i++) {
    hasil.push(a * hasil[i - 1] + (1 - a) * data[i]);
  }
  return hasil;
}

function gambar(smoothing) {
  svg.innerHTML = "";

  // garis kisi dan label sumbu
  for (let v = 0.35; v <= 1.4; v += 0.35) {
    elemen("line", { x1: M.kiri, y1: yTitik(v), x2: LEBAR - M.kanan, y2: yTitik(v), stroke: "#eceff1", "stroke-width": 1 });
    const label = elemen("text", { x: M.kiri - 6, y: yTitik(v) + 3, "text-anchor": "end", "font-size": 10, fill: "#90a4ae" });
    label.textContent = v.toFixed(2);
  }
  elemen("line", { x1: M.kiri, y1: TINGGI - M.bawah, x2: LEBAR - M.kanan, y2: TINGGI - M.bawah, stroke: "#90a4ae", "stroke-width": 1.2 });
  elemen("line", { x1: M.kiri, y1: M.atas, x2: M.kiri, y2: TINGGI - M.bawah, stroke: "#90a4ae", "stroke-width": 1.2 });
  const xlabel = elemen("text", { x: (M.kiri + LEBAR - M.kanan) / 2, y: TINGGI - 8, "text-anchor": "middle", "font-size": 11, fill: "#455a64" });
  xlabel.textContent = "langkah pencatatan";
  const ylabel = elemen("text", { x: 14, y: TINGGI / 2, "text-anchor": "middle", "font-size": 11, fill: "#455a64", transform: "rotate(-90 14 " + TINGGI / 2 + ")" });
  ylabel.textContent = "loss";

  // kurva mentah (pucat) dan kurva ter-smoothing (tebal)
  garisData(runA, WARNA.a, 1.2, 0.3);
  garisData(runB, WARNA.b, 1.2, 0.3);
  if (smoothing > 0) {
    garisData(ema(runA, smoothing), WARNA.a, 2.2, 1);
    garisData(ema(runB, smoothing), WARNA.b, 2.2, 1);
  }
}

const slider = document.getElementById("smoothing");
const labelNilai = document.getElementById("nilai-smoothing");
slider.addEventListener("input", () => {
  labelNilai.textContent = Number(slider.value).toFixed(2);
  gambar(Number(slider.value));
});

gambar(0);

</script>
<script>
// Simulasi sederhana dasbor SCALARS TensorBoard: kurva loss dua run
// dengan penggeser smoothing (rata-rata bergerak eksponensial).

const runA = [1.2950, 1.1162, 1.0401, 0.9686, 0.8502, 0.8086, 0.7459, 0.6278, 0.6735, 0.6125, 0.5276, 0.5054, 0.4947, 0.4368, 0.4093, 0.3414, 0.3886, 0.3527, 0.3391, 0.2590, 0.3547, 0.2884, 0.2568, 0.3299, 0.2469, 0.1884, 0.2165, 0.1429, 0.2528, 0.1952];
const runB = [1.3185, 1.3113, 1.1496, 1.1640, 1.0148, 1.0093, 0.9392, 0.9846, 0.9503, 0.8349, 0.8365, 0.7638, 0.7554, 0.6767, 0.6128, 0.5810, 0.6309, 0.6711, 0.5784, 0.5287, 0.5934, 0.5155, 0.4926, 0.4810, 0.4517, 0.4306, 0.3773, 0.4320, 0.3989, 0.4325];

const WARNA = { a: "#3949ab", b: "#c62828" };
const M = { kiri: 46, kanan: 12, atas: 12, bawah: 30 };
const LEBAR = 560, TINGGI = 240;
const YMAKS = 1.4;
const svg = document.getElementById("grafik");

function xTitik(i, n) {
  return M.kiri + i / (n - 1) * (LEBAR - M.kiri - M.kanan);
}

function yTitik(nilai) {
  return M.atas + (1 - nilai / YMAKS) * (TINGGI - M.atas - M.bawah);
}

function elemen(nama, atribut) {
  const el = document.createElementNS("http://www.w3.org/2000/svg", nama);
  for (const k in atribut) el.setAttribute(k, atribut[k]);
  svg.appendChild(el);
  return el;
}

function garisData(data, warna, tebal, opasitas) {
  const titik = data.map((v, i) => xTitik(i, data.length).toFixed(1) + "," + yTitik(v).toFixed(1)).join(" ");
  elemen("polyline", { points: titik, fill: "none", stroke: warna, "stroke-width": tebal, opacity: opasitas });
}

function ema(data, a) {
  const hasil = [data[0]];
  for (let i = 1; i < data.length; i++) {
    hasil.push(a * hasil[i - 1] + (1 - a) * data[i]);
  }
  return hasil;
}

function gambar(smoothing) {
  svg.innerHTML = "";

  // garis kisi dan label sumbu
  for (let v = 0.35; v <= 1.4; v += 0.35) {
    elemen("line", { x1: M.kiri, y1: yTitik(v), x2: LEBAR - M.kanan, y2: yTitik(v), stroke: "#eceff1", "stroke-width": 1 });
    const label = elemen("text", { x: M.kiri - 6, y: yTitik(v) + 3, "text-anchor": "end", "font-size": 10, fill: "#90a4ae" });
    label.textContent = v.toFixed(2);
  }
  elemen("line", { x1: M.kiri, y1: TINGGI - M.bawah, x2: LEBAR - M.kanan, y2: TINGGI - M.bawah, stroke: "#90a4ae", "stroke-width": 1.2 });
  elemen("line", { x1: M.kiri, y1: M.atas, x2: M.kiri, y2: TINGGI - M.bawah, stroke: "#90a4ae", "stroke-width": 1.2 });
  const xlabel = elemen("text", { x: (M.kiri + LEBAR - M.kanan) / 2, y: TINGGI - 8, "text-anchor": "middle", "font-size": 11, fill: "#455a64" });
  xlabel.textContent = "langkah pencatatan";
  const ylabel = elemen("text", { x: 14, y: TINGGI / 2, "text-anchor": "middle", "font-size": 11, fill: "#455a64", transform: "rotate(-90 14 " + TINGGI / 2 + ")" });
  ylabel.textContent = "loss";

  // kurva mentah (pucat) dan kurva ter-smoothing (tebal)
  garisData(runA, WARNA.a, 1.2, 0.3);
  garisData(runB, WARNA.b, 1.2, 0.3);
  if (smoothing > 0) {
    garisData(ema(runA, smoothing), WARNA.a, 2.2, 1);
    garisData(ema(runB, smoothing), WARNA.b, 2.2, 1);
  }
}

const slider = document.getElementById("smoothing");
const labelNilai = document.getElementById("nilai-smoothing");
slider.addEventListener("input", () => {
  labelNilai.textContent = Number(slider.value).toFixed(2);
  gambar(Number(slider.value));
});

gambar(0);

</script></body>
</html>

Tag yang tertata#

Tag menentukan cara kurva dikelompokkan pada dasbor, sehingga penamaannya perlu disengaja. Tag yang memuat garis miring seperti pelatihan/loss membentuk kelompok pelatihan pada panel dasbor, dan tag yang sama dari beberapa run otomatis disandingkan satu di samping yang lain. Prinsipnya, nama eksperimen diletakkan pada folder, sedangkan tag tetap singkat dan sama antara eksperimen yang ingin dibandingkan. Selain add_scalar, SummaryWriter juga menyediakan metode seperti add_image untuk citra, add_histogram untuk distribusi bobot, dan add_graph untuk arsitektur model, sehingga keluarga metode ini mencakup lebih dari sekadar angka. Metrik validasi juga layak dicatat dengan tag terpisah, misalnya validasi/loss, sehingga kurva pelatihan dan validasi dapat disandingkan dan gejala overfit terlihat ketika keduanya saling menjauh.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat TensorBoard mulai digunakan.

Yang pertama adalah lupa memanggil writer.close() atau writer.flush(). SummaryWriter menulis ke disk secara berkala, sehingga akhir pelatihan yang berhenti mendadak dapat meninggalkan sebagian catatan yang belum tertulis. Perbaikannya, tutup SummaryWriter dengan close() setelah perulangan pelatihan selesai.

Yang kedua adalah menanamkan nama eksperimen pada tag, misalnya loss-eksperimen-1. Kurva tersebut tidak dapat disandingkan dengan loss-eksperimen-2 karena kedua tag berbeda, padahal penyandingan itulah kekuatan utama dasbornya. Perbaikannya, pindahkan nama eksperimen ke nama folder dan jaga tag tetap sama.

Yang ketiga adalah mencatat nilai pada setiap batch dengan global_step yang direset tiap epoch. Kurva menjadi kacau karena sumbu mendatarnya berulang dari awal di setiap epoch. Perbaikannya, gunakan penghitung yang terus bertambah sepanjang pelatihan, atau cukup catat rata-rata loss per epoch.

Ringkasan#

  • SummaryWriter menulis event file ke folder eksperimen, dan add_scalar(tag, nilai, global_step) mencatat satu titik kurva dengan tag sebagai nama kurva.
  • Perintah tensorboard --logdir runs memulai dasbor di http://localhost:6006, sedangkan tensorboard --inspect membaca tag dan statistiknya tanpa server.
  • Nama eksperimen diletakkan pada folder dan tag dijaga tetap sama antara eksperimen, sehingga kurva dari beberapa run dapat dibandingkan secara langsung.

Referensi#