TensorBoardとW&Bによる実験トラッキング入門

実験トラッキングは、機械学習プロジェクトを効率的に管理するために不可欠です。TensorBoardとWeights & Biases(W&B)は、最も広く使われているツールです。

本記事では、両ツールの基本的な使い方と、実践的な実験管理のテクニックを解説します。

本記事の内容

  • 実験トラッキングの重要性
  • TensorBoardの使い方
  • Weights & Biasesの使い方
  • ハイパーパラメータスイープ
  • 実験の比較と分析

実験トラッキングの重要性

なぜ実験管理が必要か

  1. 再現性: どの実験がどのパラメータで行われたか記録
  2. 比較: 複数の実験結果を並べて比較
  3. デバッグ: 訓練過程を可視化して問題を発見
  4. コラボレーション: チームで実験結果を共有

記録すべき情報

カテゴリ 内容
メトリクス 損失、精度、学習率など
ハイパーパラメータ モデル構成、最適化設定など
コード 実験時のコードバージョン
データ 使用したデータセット、前処理
環境 ハードウェア、ライブラリバージョン

TensorBoard

基本的な使い方

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.tensorboard import SummaryWriter
import numpy as np

# SummaryWriterの作成
writer = SummaryWriter('runs/experiment_1')

# スカラー値のロギング
for step in range(100):
    loss = 1.0 / (step + 1)
    accuracy = 1 - loss
    learning_rate = 0.001 * (0.95 ** step)

    writer.add_scalar('Loss/train', loss, step)
    writer.add_scalar('Accuracy/train', accuracy, step)
    writer.add_scalar('Learning_rate', learning_rate, step)

# 複数のスカラーを同じグラフに
for step in range(100):
    train_loss = 1.0 / (step + 1)
    val_loss = 1.2 / (step + 1)

    writer.add_scalars('Loss', {
        'train': train_loss,
        'validation': val_loss
    }, step)

writer.close()

ヒストグラムと分布

# モデルパラメータの分布をロギング
model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 10)
)

writer = SummaryWriter('runs/histogram_example')

for epoch in range(10):
    # パラメータの分布をロギング
    for name, param in model.named_parameters():
        writer.add_histogram(f'Parameters/{name}', param, epoch)

        if param.grad is not None:
            writer.add_histogram(f'Gradients/{name}', param.grad, epoch)

writer.close()

画像のロギング

import torchvision
import matplotlib.pyplot as plt

writer = SummaryWriter('runs/images_example')

# ダミー画像
images = torch.randn(16, 3, 32, 32)

# 画像グリッドをロギング
img_grid = torchvision.utils.make_grid(images, nrow=4)
writer.add_image('Sample Images', img_grid, 0)

# matplotlibの図をロギング
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(np.sin(np.linspace(0, 10, 100)))
ax.set_title('Sine Wave')
writer.add_figure('Matplotlib Figure', fig, 0)
plt.close(fig)

writer.close()

モデルグラフ

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Linear(128, 10)
)

writer = SummaryWriter('runs/graph_example')

# ダミー入力でモデルグラフを追加
dummy_input = torch.randn(1, 784)
writer.add_graph(model, dummy_input)

writer.close()

完全な訓練ループ

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
from torch.utils.tensorboard import SummaryWriter
from datetime import datetime

class TensorBoardTrainer:
    """TensorBoardを使った訓練クラス"""

    def __init__(self, model, train_loader, val_loader, config):
        self.model = model
        self.train_loader = train_loader
        self.val_loader = val_loader
        self.config = config
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        self.model.to(self.device)

        # TensorBoardの設定
        run_name = f"{config['name']}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
        self.writer = SummaryWriter(f"runs/{run_name}")

        # ハイパーパラメータをロギング
        self.writer.add_text('config', str(config))

        self.criterion = nn.CrossEntropyLoss()
        self.optimizer = optim.Adam(model.parameters(), lr=config['lr'])
        self.scheduler = optim.lr_scheduler.StepLR(
            self.optimizer, step_size=config['lr_step'], gamma=config['lr_gamma']
        )

    def train_epoch(self, epoch):
        self.model.train()
        total_loss = 0
        correct = 0
        total = 0

        for batch_idx, (data, target) in enumerate(self.train_loader):
            data, target = data.to(self.device), target.to(self.device)

            self.optimizer.zero_grad()
            output = self.model(data)
            loss = self.criterion(output, target)
            loss.backward()
            self.optimizer.step()

            total_loss += loss.item()
            pred = output.argmax(dim=1)
            correct += (pred == target).sum().item()
            total += target.size(0)

            # バッチごとのロギング
            global_step = epoch * len(self.train_loader) + batch_idx
            self.writer.add_scalar('Loss/train_step', loss.item(), global_step)

        avg_loss = total_loss / len(self.train_loader)
        accuracy = correct / total

        return avg_loss, accuracy

    def validate(self, epoch):
        self.model.eval()
        total_loss = 0
        correct = 0
        total = 0

        with torch.no_grad():
            for data, target in self.val_loader:
                data, target = data.to(self.device), target.to(self.device)
                output = self.model(data)
                loss = self.criterion(output, target)

                total_loss += loss.item()
                pred = output.argmax(dim=1)
                correct += (pred == target).sum().item()
                total += target.size(0)

        avg_loss = total_loss / len(self.val_loader)
        accuracy = correct / total

        return avg_loss, accuracy

    def train(self, epochs):
        for epoch in range(epochs):
            train_loss, train_acc = self.train_epoch(epoch)
            val_loss, val_acc = self.validate(epoch)

            self.scheduler.step()
            current_lr = self.scheduler.get_last_lr()[0]

            # エポックごとのロギング
            self.writer.add_scalars('Loss', {
                'train': train_loss,
                'val': val_loss
            }, epoch)

            self.writer.add_scalars('Accuracy', {
                'train': train_acc,
                'val': val_acc
            }, epoch)

            self.writer.add_scalar('Learning_rate', current_lr, epoch)

            # パラメータの分布
            for name, param in self.model.named_parameters():
                self.writer.add_histogram(f'weights/{name}', param, epoch)
                if param.grad is not None:
                    self.writer.add_histogram(f'grads/{name}', param.grad, epoch)

            print(f"Epoch {epoch}: train_loss={train_loss:.4f}, "
                  f"val_loss={val_loss:.4f}, val_acc={val_acc:.4f}")

        self.writer.close()


# 使用例
model = nn.Sequential(
    nn.Flatten(),
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Dropout(0.2),
    nn.Linear(256, 10)
)

config = {
    'name': 'mnist_experiment',
    'lr': 0.001,
    'lr_step': 10,
    'lr_gamma': 0.5,
    'batch_size': 64,
}

# ダミーデータ
train_data = TensorDataset(torch.randn(1000, 1, 28, 28), torch.randint(0, 10, (1000,)))
val_data = TensorDataset(torch.randn(200, 1, 28, 28), torch.randint(0, 10, (200,)))
train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
val_loader = DataLoader(val_data, batch_size=64)

# trainer = TensorBoardTrainer(model, train_loader, val_loader, config)
# trainer.train(epochs=20)

TensorBoardの起動

# TensorBoardサーバーを起動
tensorboard --logdir=runs --port=6006

# ブラウザで http://localhost:6006 にアクセス

Weights & Biases

基本的な使い方

import wandb
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# W&Bの初期化
wandb.init(
    project="my-ml-project",
    name="experiment-1",
    config={
        "learning_rate": 0.001,
        "epochs": 100,
        "batch_size": 64,
        "architecture": "MLP",
        "dataset": "MNIST",
    }
)

# スカラー値のロギング
for step in range(100):
    loss = 1.0 / (step + 1)
    accuracy = 1 - loss

    wandb.log({
        "loss": loss,
        "accuracy": accuracy,
        "step": step
    })

wandb.finish()

完全な訓練ループ

import wandb
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

def train_with_wandb():
    # W&Bの初期化
    config = {
        "learning_rate": 0.001,
        "epochs": 20,
        "batch_size": 64,
        "hidden_size": 256,
        "dropout": 0.2,
    }

    run = wandb.init(
        project="mnist-experiments",
        config=config,
        tags=["baseline", "mlp"]
    )

    # configはwandb.configからアクセス
    config = wandb.config

    # モデル定義
    model = nn.Sequential(
        nn.Flatten(),
        nn.Linear(784, config.hidden_size),
        nn.ReLU(),
        nn.Dropout(config.dropout),
        nn.Linear(config.hidden_size, 10)
    )

    # W&Bでモデルを監視
    wandb.watch(model, log="all", log_freq=100)

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model.to(device)

    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=config.learning_rate)

    # ダミーデータ
    train_data = TensorDataset(
        torch.randn(1000, 1, 28, 28),
        torch.randint(0, 10, (1000,))
    )
    val_data = TensorDataset(
        torch.randn(200, 1, 28, 28),
        torch.randint(0, 10, (200,))
    )
    train_loader = DataLoader(train_data, batch_size=config.batch_size, shuffle=True)
    val_loader = DataLoader(val_data, batch_size=config.batch_size)

    # 訓練ループ
    for epoch in range(config.epochs):
        model.train()
        train_loss = 0
        train_correct = 0
        train_total = 0

        for data, target in train_loader:
            data, target = data.to(device), target.to(device)

            optimizer.zero_grad()
            output = model(data)
            loss = criterion(output, target)
            loss.backward()
            optimizer.step()

            train_loss += loss.item()
            pred = output.argmax(dim=1)
            train_correct += (pred == target).sum().item()
            train_total += target.size(0)

        # 検証
        model.eval()
        val_loss = 0
        val_correct = 0
        val_total = 0

        with torch.no_grad():
            for data, target in val_loader:
                data, target = data.to(device), target.to(device)
                output = model(data)
                loss = criterion(output, target)

                val_loss += loss.item()
                pred = output.argmax(dim=1)
                val_correct += (pred == target).sum().item()
                val_total += target.size(0)

        # メトリクスをロギング
        wandb.log({
            "epoch": epoch,
            "train/loss": train_loss / len(train_loader),
            "train/accuracy": train_correct / train_total,
            "val/loss": val_loss / len(val_loader),
            "val/accuracy": val_correct / val_total,
        })

    # モデルの保存
    torch.save(model.state_dict(), "model.pt")
    wandb.save("model.pt")

    wandb.finish()


# train_with_wandb()

画像とテーブルのロギング

import wandb
import numpy as np
import matplotlib.pyplot as plt

wandb.init(project="visualization-demo")

# 画像のロギング
images = np.random.rand(16, 32, 32, 3)
wandb.log({
    "images": [wandb.Image(img, caption=f"Image {i}") for i, img in enumerate(images[:4])]
})

# matplotlibの図
fig, ax = plt.subplots()
ax.plot(np.sin(np.linspace(0, 10, 100)))
ax.set_title("Sine Wave")
wandb.log({"sine_wave": wandb.Image(fig)})
plt.close(fig)

# テーブルのロギング
data = [
    ["cat", 0.95, 0.98],
    ["dog", 0.90, 0.92],
    ["bird", 0.85, 0.88],
]
table = wandb.Table(columns=["class", "precision", "recall"], data=data)
wandb.log({"metrics_table": table})

# 混同行列
confusion_matrix = np.random.randint(0, 100, (5, 5))
wandb.log({
    "confusion_matrix": wandb.plot.confusion_matrix(
        y_true=[0, 1, 2, 3, 4] * 20,
        preds=np.random.randint(0, 5, 100).tolist(),
        class_names=["A", "B", "C", "D", "E"]
    )
})

wandb.finish()

ハイパーパラメータスイープ

import wandb

# スイープ設定
sweep_config = {
    'method': 'bayes',  # 'grid', 'random', 'bayes'
    'metric': {
        'name': 'val/accuracy',
        'goal': 'maximize'
    },
    'parameters': {
        'learning_rate': {
            'min': 0.0001,
            'max': 0.01,
            'distribution': 'log_uniform_values'
        },
        'hidden_size': {
            'values': [64, 128, 256, 512]
        },
        'dropout': {
            'min': 0.0,
            'max': 0.5
        },
        'batch_size': {
            'values': [32, 64, 128]
        }
    }
}


def train_sweep():
    """スイープ用の訓練関数"""
    run = wandb.init()
    config = wandb.config

    # モデル構築
    model = nn.Sequential(
        nn.Flatten(),
        nn.Linear(784, config.hidden_size),
        nn.ReLU(),
        nn.Dropout(config.dropout),
        nn.Linear(config.hidden_size, 10)
    )

    # ... 訓練ループ ...

    # 最終的な精度をログ
    wandb.log({"val/accuracy": 0.95})  # 実際の値に置き換え


# スイープの作成と実行
# sweep_id = wandb.sweep(sweep_config, project="sweep-demo")
# wandb.agent(sweep_id, train_sweep, count=20)

Artifacts(モデルとデータの管理)

import wandb
import torch

wandb.init(project="artifacts-demo")

# モデルをアーティファクトとして保存
model = nn.Linear(10, 5)
torch.save(model.state_dict(), "model.pt")

artifact = wandb.Artifact("my-model", type="model")
artifact.add_file("model.pt")
wandb.log_artifact(artifact)

# データセットをアーティファクトとして保存
data_artifact = wandb.Artifact("my-dataset", type="dataset")
data_artifact.add_dir("data/")
wandb.log_artifact(data_artifact)

# アーティファクトの使用
artifact = wandb.use_artifact("my-model:latest")
artifact_dir = artifact.download()

wandb.finish()

TensorBoard vs W&B

比較表

機能 TensorBoard Weights & Biases
価格 無料 個人無料、チーム有料
ホスティング ローカル クラウド
スカラー あり あり
ヒストグラム あり あり
画像 あり あり
モデルグラフ あり あり
ハイパラスイープ なし(外部ツール) 組み込み
実験比較 基本的 高度
コラボレーション 難しい 容易
オフライン 可能 可能(同期は後)

使い分けの指針

TensorBoardを選ぶ場合: – ローカルでの開発・デバッグ – シンプルなプロジェクト – コスト重視

W&Bを選ぶ場合: – チームでの共同作業 – ハイパーパラメータ最適化 – 実験の長期管理

両方を使う

import wandb
from torch.utils.tensorboard import SummaryWriter

class DualLogger:
    """TensorBoardとW&Bの両方にロギング"""

    def __init__(self, config, project_name, run_name):
        self.tb_writer = SummaryWriter(f'runs/{run_name}')
        wandb.init(project=project_name, name=run_name, config=config)

    def log_scalar(self, name, value, step):
        self.tb_writer.add_scalar(name, value, step)
        wandb.log({name: value}, step=step)

    def log_scalars(self, main_tag, tag_scalar_dict, step):
        self.tb_writer.add_scalars(main_tag, tag_scalar_dict, step)
        wandb.log({f"{main_tag}/{k}": v for k, v in tag_scalar_dict.items()}, step=step)

    def log_histogram(self, name, values, step):
        self.tb_writer.add_histogram(name, values, step)
        wandb.log({f"histogram/{name}": wandb.Histogram(values.cpu().numpy())}, step=step)

    def close(self):
        self.tb_writer.close()
        wandb.finish()

まとめ

本記事では、実験トラッキングツールについて解説しました。

  • TensorBoard: ローカルで動作、PyTorchと統合、無料
  • Weights & Biases: クラウドベース、ハイパラスイープ、チーム機能
  • ロギング対象: メトリクス、ハイパーパラメータ、画像、モデル構造
  • ベストプラクティス: 一貫した命名規則、定期的なロギング、コードのバージョン管理

実験トラッキングは、機械学習プロジェクトの再現性と効率を大きく向上させます。

次のステップとして、以下の記事も参考にしてください。