[PyTorch 學習筆記] 5.1 TensorBoard 介紹

張賢同學發表於2020-09-05

本章程式碼:

這篇文章主要介紹了 PyTorch 中的優化器,包括 3 個部分:優化器的概念、optimizer 的屬性、optimizer 的方法。

TensorBoard 是 TensorFlow 中強大的視覺化工具,支援標量、文字、影像、音訊、視訊和 Embedding 等多種資料視覺化。

在 PyTorch 中也可以使用 TensorBoard,具體是使用 TensorboardX 來呼叫 TensorBoard。除了安裝 TensorboardX,還要安裝 TensorFlow 和 TensorBoard,其中 TensorFlow 和 TensorBoard 需要一致。

TensorBoardX 視覺化的流程需要首先編寫 Python 程式碼把需要視覺化的資料儲存到 event file 檔案中,然後再使用 TensorBoardX 讀取 event file 展示到網頁中。

下面的程式碼是一個儲存 event file 的例子:

    import numpy as np
    import matplotlib.pyplot as plt
    from tensorboardX import SummaryWriter
    from common_tools import set_seed
    max_epoch = 100

    writer = SummaryWriter(comment='test_comment', filename_suffix="test_suffix")

    for x in range(max_epoch):

        writer.add_scalar('y=2x', x * 2, x)
        writer.add_scalar('y=pow_2_x', 2 ** x, x)

        writer.add_scalars('data/scalar_group', {"xsinx": x * np.sin(x),
                                                 "xcosx": x * np.cos(x)}, x)

    writer.close()

上面具體儲存的資料,我們先不關注,主要關注的是儲存 event file 需要用到 SummaryWriter 類,這個類是用於儲存資料的最重要的類,執行完後,會在當前資料夾生成一個runs的資料夾,裡面儲存的就是資料的 event file。

然後在命令列中輸入tensorboard --logdir=lesson5/runs啟動 tensorboard 服務,其中lesson5/runsruns資料夾的路徑。然後命令列會顯示 tensorboard 的訪問地址:

TensorBoard 1.9.0 at http://LAPTOP-DPDNNJSU:6006 (Press CTRL+C to quit)

在瀏覽器中開啟,顯示如下:

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

最上面的一欄顯示的是資料型別,由於我們在程式碼中只記錄了 scalar 型別的資料,因此只顯示`SCALARS`。

右上角有一些功能設定

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

點選`INACTIVE`顯示我們沒有記錄的資料型別。設定裡可以設定重新整理 tensorboard 的間隔,在模型訓練時可以實時監控資料的變化。

左邊的選單欄如下,點選Show data download links可以展示每個圖的下載按鈕,如果一個圖中有多個資料,需要選中需要下載的曲線,然後下載,格式有 csvjson可選。

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

第二個選項`Ignore outliers in chart scaling`可以設定是否忽略離群點,在`y_pow_2_x`中,資料的尺度達到了 $10^{18}$,勾選`Ignore outliers in chart scaling`後 $y$ 軸的尺度下降到 $10^{17}$。
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

Soothing 是對影像進行平滑,下圖中,顏色較淡的陰影部分才是真正的曲線資料,Smoothing 設定為了 0.6,進行了平滑才展示為顏色較深的線。
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

Smoothing 設定為 0,沒有進行平滑,顯示如下:
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

Smoothing 設定為 1,則平滑後的線和 $x$ 軸重合,顯示如下:
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

`Horizontal Axis`表示橫軸:`STEP`表示原始資料作為橫軸,`RELATIVE`和`WALL`都是以時間作為橫軸,單位是小時,`RELATIVE`是相對時間,`WALL`是絕對時間。

runs顯示所有的 event file,可以選擇展示某些 event file 的影像,其中正方形按鈕是多選,圓形按鈕是單選。

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

上面的搜尋框可以根據 tags 來搜尋資料對應的影像
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

# optimizer 的屬性

PyTorch 中提供了 Optimizer 類,定義如下:

class Optimizer(object):
	def __init__(self, params, defaults):
		self.defaults = defaults
        self.state = defaultdict(dict)
        self.param_groups = []

主要有 3 個屬性

  • defaults:優化器的超引數,如 weight_decay,momentum
  • state:引數的快取,如 momentum 中需要用到前幾次的梯度,就快取在這個變數中
  • param_groups:管理的引數組,是一個 list,其中每個元素是字典,包括 momentum、lr、weight_decay、params 等。
  • _step_count:記錄更新 次數,在學習率調整中使用

SummaryWriter

torch.utils.tensorboard.writer.SummaryWriter(log_dir=None, comment='', purge_step=None, max_queue=10, flush_secs=120, filename_suffix='')

功能:提供建立 event file 的高階介面

主要功能:

  • log_dir:event file 輸出資料夾,預設為runs資料夾
  • comment:不指定 log_dir 時,runs資料夾裡的子資料夾字尾
  • filename_suffix:event_file 檔名字尾

程式碼如下:

    log_dir = "./train_log/test_log_dir"
    writer = SummaryWriter(log_dir=log_dir, comment='_scalars', filename_suffix="12345678")
    # writer = SummaryWriter(comment='_scalars', filename_suffix="12345678")

    for x in range(100):
        writer.add_scalar('y=pow_2_x', 2 ** x, x)

    writer.close()

執行後會生成train_log/test_log_dir資料夾,裡面的 event file 檔名字尾是12345678

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

但是我們指定了`log_dir`,`comment`引數沒有生效。如果想要`comment`引數生效,把`SummaryWriter`的初始化改為`writer = SummaryWriter(comment='_scalars', filename_suffix="12345678")`,生成的資料夾如下,`runs`裡的子資料夾字尾是`_scalars`。
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

# add_scalar
add_scalar(tag, scalar_value, global_step=None, walltime=None)

功能:記錄標量

  • tag:影像的標籤名,圖的唯一標識
  • scalar_value:要記錄的標量,y 軸的資料
  • global_step:x 軸的資料

add_scalars

上面的add_scalar()只能記錄一條曲線的資料。但是我們在實際中可能需要在一張圖中同時展示多條曲線,比如在訓練模型時,經常需要同時檢視訓練集和測試集的 loss。這時我們可以使用add_scalars()方法

add_scalars(main_tag, tag_scalar_dict, global_step=None, walltime=None)
  • main_tag:該圖的標籤
  • tag_scalar_dict:用字典的形式記錄多個曲線。key 是變數的 tag,value 是變數的值

程式碼如下:

    max_epoch = 100
    writer = SummaryWriter(comment='test_comment', filename_suffix="test_suffix")
    for x in range(max_epoch):
        writer.add_scalar('y=2x', x * 2, x)
        writer.add_scalar('y=pow_2_x', 2 ** x, x)
        writer.add_scalars('data/scalar_group', {"xsinx": x * np.sin(x),
                                                 "xcosx": x * np.cos(x)}, x)
    writer.close()

執行後生成 event file,然後使用 TensorBoard 來檢視如下:

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

每個影像下面都有 3 個按鈕,中間的按鈕是以對數形式展示 y 軸。如對`y=pow_2_x`曲線的 y 軸取對數展示如下,變成了直線。
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

# add_histogram
add_histogram(tag, values, global_step=None, bins='tensorflow', walltime=None, max_bins=None)

功能:統計直方圖與多分位折線圖

  • tag:影像的標籤名,圖的唯一標識
  • values:要統計的引數,通常統計權值、偏置或者梯度
  • global_step:第幾個子圖
  • bins:取直方圖的 bins

下面的程式碼構造了均勻分佈和正態分佈,迴圈生成了 2 次,分別用matplotlib和 TensorBoard 進行畫圖。

    writer = SummaryWriter(comment='test_comment', filename_suffix="test_suffix")
    for x in range(2):
        np.random.seed(x)
        data_union = np.arange(100)
        data_normal = np.random.normal(size=1000)
        writer.add_histogram('distribution union', data_union, x)
        writer.add_histogram('distribution normal', data_normal, x)
        plt.subplot(121).hist(data_union, label="union")
        plt.subplot(122).hist(data_normal, label="normal")
        plt.legend()
        plt.show()
    writer.close()

matplotlib畫圖顯示如下:

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

TensorBoard 顯示結果如下。

正態分佈顯示如下,每個子圖分別對應一個 global_step:

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

均勻分佈顯示如下,顯示曲線的原因和`bins`引數設定有關,預設是`tensorflow`:
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

除此之外,還會得到`DISTRIBUTIONS`,這是多分位折線圖,縱軸有 9 個折線,表示資料的分佈區間,某個區間的顏色越深,表示這個區間的數所佔比例越大。橫軸是 global_step。這個圖的作用是觀察數方差的變化情況。顯示如下:
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

# 模型指標監控

下面使用 TensorBoard 來監控人民幣二分類實驗訓練過程中的 loss、accuracy、weights 和 gradients 的變化情況。

首先定義一個SummaryWriter

writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

然後在每次訓練中記錄 loss 和 accuracy 的值

# 記錄資料,儲存於event file
writer.add_scalars("Loss", {"Train": loss.item()}, iter_count)
writer.add_scalars("Accuracy", {"Train": correct / total}, iter_count)

並且在驗證時記錄所有驗證集樣本的 loss 和 accuracy 的均值

# 記錄資料,儲存於event file
writer.add_scalars("Loss", {"Valid": np.mean(valid_curve)}, iter_count)
writer.add_scalars("Accuracy", {"Valid": correct / total}, iter_count)

並且在每個 epoch 中記錄每一層權值以及權值的梯度。

    # 每個epoch,記錄梯度,權值
    for name, param in net.named_parameters():
        writer.add_histogram(name + '_grad', param.grad, epoch)
        writer.add_histogram(name + '_data', param, epoch)

在訓練還沒結束時,就可以啟動 TensorBoard 視覺化,Accuracy 的視覺化如下,顏色較深的是訓練集的 Accuracy,顏色較淺的是 驗證集的樣本:

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

Loss 的視覺化如下,其中驗證集的 Loss 是從第 10 個 epoch 才開始記錄的,並且 驗證集的 Loss 是所有驗證集樣本的 Loss 均值,所以曲線更加平滑;而訓練集的 Loss 是 batch size 的資料,因此震盪幅度較大:
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

上面的 Loss 曲線圖與使用`matplotlib`畫的圖不太一樣,因為 TensorBoard 預設會進行 Smoothing,我們把 Smoothing 係數設定為 0 後,顯示如下:
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

而記錄權值以及權值梯度的 HISTOGRAMS 顯示如下,記錄了每一層的資料:
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

展開檢視第一層的權值和梯度。
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

可以看到每一個 epoch 的梯度都是呈正態分佈,說明權值分佈比較好;梯度都是接近於 0,說明模型很快就收斂了。通常我們使用 TensorBoard 檢視我們的網路引數在訓練時的分佈變化情況,如果分佈很奇怪,並且 Loss 沒有下降,這時需要考慮是什麼原因改變了資料的分佈較大的。如果前面網路層的梯度很小,後面網路層的梯度比較大,那麼可能是梯度消失,因為後面網路層的較大梯度反向傳播到前面網路層時已經變小了。如果前後網路層的梯度都很小,那麼說明不是梯度消失,而是因為 Loss 很小,模型已經接近收斂。

add_image

add_image(tag, img_tensor, global_step=None, walltime=None, dataformats='CHW')

功能:記錄影像

  • tag:影像的標籤名,影像的唯一標識
  • img_tensor:影像資料,需要注意尺度
  • global_step:記錄這是第幾個子圖
  • dataformats:資料形式,取值有'CHW','HWC','HW'。如果畫素值在 [0, 1] 之間,那麼預設會乘以 255,放大到 [0, 255] 範圍之間。如果有大於 1 的畫素值,認為已經是 [0, 255] 範圍,那麼就不會放大。

程式碼如下:

writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

# img 1     random
# 隨機噪聲的圖片
fake_img = torch.randn(3, 512, 512)
writer.add_image("fake_img", fake_img, 1)
time.sleep(1)

# img 2     ones
# 畫素值全為 1 的圖片,會乘以 255,所以是白色的圖片
fake_img = torch.ones(3, 512, 512)
time.sleep(1)
writer.add_image("fake_img", fake_img, 2)

# img 3     1.1
# 畫素值全為 1.1 的圖片,不會乘以 255,所以是黑色的圖片
fake_img = torch.ones(3, 512, 512) * 1.1
time.sleep(1)
writer.add_image("fake_img", fake_img, 3)

# img 4     HW
fake_img = torch.rand(512, 512)
writer.add_image("fake_img", fake_img, 4, dataformats="HW")

# img 5     HWC
fake_img = torch.rand(512, 512, 3)
writer.add_image("fake_img", fake_img, 5, dataformats="HWC")

writer.close()

使用 TensorBoard 視覺化如下:

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

圖片上面的`step`可以選擇第幾張圖片,如選擇第 3 張圖片,顯示如下:
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

# torchvision.utils.make_grid

上面雖然可以通過拖動顯示每張圖片,但實際中我們希望在網格中同時展示多張圖片,可以用到make_grid()函式。

torchvision.utils.make_grid(tensor: Union[torch.Tensor, List[torch.Tensor]], nrow: int = 8, padding: int = 2, normalize: bool = False, range: Optional[Tuple[int, int]] = None, scale_each: bool = False, pad_value: int = 0)

功能:製作網格影像

  • tensor:影像資料,$B \times C \times H \times W$ 的形狀
  • nrow:行數(列數是自動計算的,為:$\frac{B}{nrow}$)
  • padding:影像間距,單位是畫素,預設為 2
  • normalize:是否將畫素值標準化到 [0, 255] 之間
  • range:標準化範圍,例如原圖的畫素值範圍是 [-1000, 2000],設定 range 為 [-600, 500],那麼會把小於 -600 的畫素值變為 -600,那麼會把大於 500 的畫素值變為 500,然後標準化到 [0, 255] 之間
  • scale_each:是否單張圖維度標準化
  • pad_value:間隔的畫素值

下面的程式碼是人民幣圖片的網路視覺化,batch_size 設定為 16,nrow 設定為 4,得到 4 行 4 列的網路影像

writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

split_dir = os.path.join(enviroments.project_dir, "data", "rmb_split")
train_dir = os.path.join(split_dir, "train")
# train_dir = "path to your training data"
# 先把寬高縮放到 [32, 64] 之間,然後使用 toTensor 把 Image 轉化為 tensor,並把畫素值縮放到 [0, 1] 之間
transform_compose = transforms.Compose([transforms.Resize((32, 64)), transforms.ToTensor()])
train_data = RMBDataset(data_dir=train_dir, transform=transform_compose)
train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)
data_batch, label_batch = next(iter(train_loader))

img_grid = vutils.make_grid(data_batch, nrow=4, normalize=True, scale_each=True)
# img_grid = vutils.make_grid(data_batch, nrow=4, normalize=False, scale_each=False)
writer.add_image("input img", img_grid, 0)

writer.close()

TensorBoard 顯示如下:

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

# AlexNet 卷積核與特徵圖視覺化

使用 TensorBoard 視覺化 AlexNet 網路的前兩層卷積核。其中每一層的卷積核都把輸出的維度作為 global_step,包括兩種視覺化方式:一種是每個 (w, h) 維度作為灰度圖,新增一個 c 的維度,形成 (b, c, h, w),其中 b 是 輸入的維度;另一種是把整個卷積核 reshape 到 c 是 3 的形狀,再進行視覺化。詳細見如下程式碼:

    writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

    alexnet = models.alexnet(pretrained=True)

    # 當前遍歷到第幾層網路的卷積核了
    kernel_num = -1
    # 最多顯示兩層網路的卷積核:第 0 層和第 1 層
    vis_max = 1

    # 獲取網路的每一層
    for sub_module in alexnet.modules():
        # 判斷這一層是否為 2 維卷積層
        if isinstance(sub_module, nn.Conv2d):
            kernel_num += 1
            # 如果當前層大於1,則停止記錄權值
            if kernel_num > vis_max:
                break
            # 獲取這一層的權值
            kernels = sub_module.weight
            # 權值的形狀是 [c_out, c_int, k_w, k_h]
            c_out, c_int, k_w, k_h = tuple(kernels.shape)

            # 根據輸出的每個維度進行視覺化
            for o_idx in range(c_out):
                # 取出的資料形狀是 (c_int, k_w, k_h),對應 BHW; 需要擴充套件為 (c_int, 1, k_w, k_h),對應 BCHW
                kernel_idx = kernels[o_idx, :, :, :].unsqueeze(1)   # make_grid需要 BCHW,這裡擴充C維度
                # 注意 nrow 設定為 c_int,所以行數為 1。在 for 迴圈中每 新增一個,就會多一個 global_step
                kernel_grid = vutils.make_grid(kernel_idx, normalize=True, scale_each=True, nrow=c_int)
                writer.add_image('{}_Convlayer_split_in_channel'.format(kernel_num), kernel_grid, global_step=o_idx)
            # 因為 channe 為 3 時才能進行視覺化,所以這裡 reshape
            kernel_all = kernels.view(-1, 3, k_h, k_w)  #b, 3, h, w
            kernel_grid = vutils.make_grid(kernel_all, normalize=True, scale_each=True, nrow=8)  # c, h, w
            writer.add_image('{}_all'.format(kernel_num), kernel_grid, global_step=kernel_num+1)

            print("{}_convlayer shape:{}".format(kernel_num, tuple(kernels.shape)))

    writer.close()

使用 TensorBoard 視覺化如下。

這是根據輸出的維度分批展示第一層卷積核的視覺化

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

這是根據輸出的維度分批展示第二層卷積核的視覺化
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

這是整個第一層卷積核的視覺化
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

這是整個第二層卷積核的視覺化
[PyTorch 學習筆記] 5.1 TensorBoard 介紹

下面把 AlexNet 的第一個卷積層的輸出進行視覺化,首先對圖片資料進行預處理(resize,標準化等操作)。由於在定義模型時,網路層通過nn.Sequential() 堆疊,儲存在 features 變數中。因此通過 features 獲取第一個卷積層。把圖片輸入卷積層得到輸出,形狀為 (1, 64, 55, 55),需要轉換為 (64, 1, 55, 55),對應 (B, C, H, W),nrow 設定為 8,最後進行視覺化,程式碼如下:
    writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

    # 資料
    path_img = "./lena.png"     # your path to image
    normMean = [0.49139968, 0.48215827, 0.44653124]
    normStd = [0.24703233, 0.24348505, 0.26158768]

    norm_transform = transforms.Normalize(normMean, normStd)
    img_transforms = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        norm_transform
    ])

    img_pil = Image.open(path_img).convert('RGB')
    if img_transforms is not None:
        img_tensor = img_transforms(img_pil)
    img_tensor.unsqueeze_(0)    # chw --> bchw

    # 模型
    alexnet = models.alexnet(pretrained=True)

    # forward
    # 由於在定義模型時,網路層通過nn.Sequential() 堆疊,儲存在 features 變數中。因此通過 features 獲取第一個卷積層
    convlayer1 = alexnet.features[0]
    # 把圖片輸入第一個卷積層
    fmap_1 = convlayer1(img_tensor)

    # 預處理
    fmap_1.transpose_(0, 1)  # bchw=(1, 64, 55, 55) --> (64, 1, 55, 55)
    fmap_1_grid = vutils.make_grid(fmap_1, normalize=True, scale_each=True, nrow=8)

    writer.add_image('feature map in conv1', fmap_1_grid, global_step=322)
    writer.close()

使用 TensorBoard 視覺化如下:

[PyTorch 學習筆記] 5.1 TensorBoard 介紹

# add_graph
add_graph(model, input_to_model=None, verbose=False)

功能:視覺化模型計算圖

  • model:模型,必須繼承自 nn.Module
  • input_to_model:輸入給模型的資料,形狀為 BCHW
  • verbose:是否列印圖結構資訊

檢視 LeNet 的計算圖程式碼如下:

    writer = SummaryWriter(comment='test_your_comment', filename_suffix="_test_your_filename_suffix")

    # 模型
    fake_img = torch.randn(1, 3, 32, 32)
    lenet = LeNet(classes=2)
    writer.add_graph(lenet, fake_img)
    writer.close()

使用 TensorBoard 視覺化如下:

torchsummary

模型計算圖的視覺化還是比較複雜,不夠清晰。而torchsummary能夠檢視模型的輸入和輸出的形狀,可以更加清楚地輸出模型的結構。

torchsummary.summary(model, input_size, batch_size=-1, device="cuda")

功能:檢視模型的資訊,便於除錯

  • model:pytorch 模型,必須繼承自 nn.Module
  • input_size:模型輸入 size,形狀為 CHW
  • batch_size:batch_size,預設為 -1,在展示模型每層輸出的形狀時顯示的 batch_size
  • device:"cuda"或者"cpu"

檢視 LeNet 的模型資訊程式碼如下:

    # 模型
    lenet = LeNet(classes=2)
    print(summary(lenet, (3, 32, 32), device="cpu"))

輸出如下:

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1            [-1, 6, 28, 28]             456
            Conv2d-2           [-1, 16, 10, 10]           2,416
            Linear-3                  [-1, 120]          48,120
            Linear-4                   [-1, 84]          10,164
            Linear-5                    [-1, 2]             170
================================================================
Total params: 61,326
Trainable params: 61,326
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.01
Forward/backward pass size (MB): 0.05
Params size (MB): 0.23
Estimated Total Size (MB): 0.30
----------------------------------------------------------------
None

上述資訊分別有模型每層的輸出形狀,每層的引數數量,總的引數數量,以及模型大小等資訊。

我們以第一層為例,第一層卷積核大小是 (6, 3, 5, 5),每個卷積核還有一個偏置,因此 $6 \times 3 \times 5 \times 5+6=456$。

參考資料


如果你覺得這篇文章對你有幫助,不妨點個贊,讓我有更多動力寫出好文章。

相關文章