作者 | 錦恢@知乎
編輯 | 極市平台
導讀
本文大緻想說一下pytorch下的網絡結構可視化和訓練過程可視化。
一、網絡結構的可視化
我們訓練神經網絡時,除了随着step或者epoch觀察損失函數的走勢,進而建立對目前網絡優化的基本認知外,也可以通過一些額外的可視化庫來可視化我們的神經網絡結構圖。這将更加地高效地向讀者展現目前的網絡結構。
為了可視化神經網絡,我們先建立一個簡單的卷積層神經網絡:
import torch
import torch.nn as nn
class ConvNet(nn.Module):
def __init__(self):
super(ConvNet, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(1, 16, 3, 1, 1),
nn.ReLU(),
nn.AvgPool2d(2, 2)
)
self.conv2 = nn.Sequential(
nn.Conv2d(16, 32, 3, 1, 1),
nn.ReLU(),
nn.MaxPool2d(2, 2)
)
self.fc = nn.Sequential(
nn.Linear(32 * 7 * 7, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU()
)
self.out = nn.Linear(64, 10)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
output = self.out(x)
return output
輸出網絡結構:
MyConvNet = ConvNet()
print(MyConvNet)
輸出結果:
ConvNet(
(conv1): Sequential(
(0): Conv2d(1, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU()
(2): AvgPool2d(kernel_size=2, stride=2, padding=0)
)
(conv2): Sequential(
(0): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU()
(2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(fc): Sequential(
(0): Linear(in_features=1568, out_features=128, bias=True)
(1): ReLU()
(2): Linear(in_features=128, out_features=64, bias=True)
(3): ReLU()
)
(out): Linear(in_features=64, out_features=10, bias=True)
)
有了基本的神經網絡後,我們分别通過
HiddenLayer
和
PyTorchViz
庫來可視化上述的卷積層神經網絡。
需要說明的是,這兩個庫都是基于Graphviz開發的,是以倘若你的電腦上沒有安裝并且沒有添加環境變量,請自行安裝Graphviz工具,安裝教程
1.1 通過HiddenLayer可視化網絡
首先當然是安裝庫啦,打開cmd,輸入:
pip install hiddenlayer
繪制的基本程式如下:
import hiddenlayer as h
vis_graph = h.build_graph(MyConvNet, torch.zeros([1 ,1, 28, 28])) # 擷取繪制圖像的對象
vis_graph.theme = h.graph.THEMES["blue"].copy() # 指定主題顔色
vis_graph.save("./demo1.png") # 儲存圖像的路徑
效果如下:
1.2 通過PyTorchViz可視化網絡
先安裝庫:
pip install torchviz
這裡我們隻使用可視化函數
make_dot()
來擷取繪圖對象,基本使用和
HiddenLayer
差不多,不同的地方在于
PyTorch
繪圖之前可以指定一個網絡的輸入值和預測值。
from torchviz import make_dot
x = torch.randn(1, 1, 28, 28).requires_grad_(True) # 定義一個網絡的輸入值
y = MyConvNet(x) # 擷取網絡的預測值
MyConvNetVis = make_dot(y, params=dict(list(MyConvNet.named_parameters()) + [('x', x)]))
MyConvNetVis.format = "png"
# 指定檔案生成的檔案夾
MyConvNetVis.directory = "data"
# 生成檔案
MyConvNetVis.view()
打開與上述代碼相同根目錄下的data檔案夾,裡面會有一個
.gv
檔案和一個
.png
檔案,其中的
.gv
檔案是Graphviz工具生成圖檔的腳本代碼,
.png
是
.gv
檔案編譯生成的圖檔,直接打開
.png
檔案就行。
預設情況下,上述程式運作後會自動打開.png檔案
生成圖檔:
二、訓練過程可視化
觀察我們的網絡的每一步的損失函數或準确率的變化可以有效地幫助我們判斷目前訓練過程的優劣。如果能将這些過程可視化,那麼我們判斷的準确性和舒适性都會有所增加。
此處主要講通過可視化神器
tensorboardX
和剛剛用到的
HiddenLayer
來實作訓練過程的可視化。
為了訓練網絡,我們先導入訓練網絡需要的資料,此處就導入MNIST資料集,并做訓練前的一些基本的資料處理。
import torchvision
import torch.utils.data as Data
# 準備訓練用的MNIST資料集
train_data = torchvision.datasets.MNIST(
root = "./data/MNIST", # 提取資料的路徑
train=True, # 使用MNIST内的訓練資料
transform=torchvision.transforms.ToTensor(), # 轉換成torch.tensor
download=False # 如果是第一次運作的話,置為True,表示下載下傳資料集到root目錄
)
# 定義loader
train_loader = Data.DataLoader(
dataset=train_data,
batch_size=128,
shuffle=True,
num_workers=0
)
test_data = torchvision.datasets.MNIST(
root="./data/MNIST",
train=False, # 使用測試資料
download=False
)
# 将測試資料壓縮到0-1
test_data_x = test_data.data.type(torch.FloatTensor) / 255.0
test_data_x = torch.unsqueeze(test_data_x, dim=1)
test_data_y = test_data.targets
# 列印一下測試資料和訓練資料的shape
print("test_data_x.shape:", test_data_x.shape)
print("test_data_y.shape:", test_data_y.shape)
for x, y in train_loader:
print(x.shape)
print(y.shape)
break
結果:
test_data_x.shape: torch.Size([10000, 1, 28, 28])
test_data_y.shape: torch.Size([10000])
torch.Size([128, 1, 28, 28])
torch.Size([128])
2.1 通過tensorboardX可視化訓練過程
tensorboard
是谷歌開發的深度學習架構tensorflow的一套深度學習可視化神器,在pytorch團隊的努力下,他們開發出了tensorboardX來讓pytorch的玩家也能享受tensorboard的福利。
先安裝相關的庫:
pip install tensorboardX
pip install tensorboard
并将tensorboard.exe所在的檔案夾路徑加入環境變量path中(比如我的tensorboard.exe的路徑為
D:\Python376\Scripts\tensorboard.exe
,那麼就在path中加入
D:\Python376\Scripts
)
下面是
tensorboardX
的使用過程。基本使用為,先通過
tensorboardX
下的
SummaryWriter
類擷取一個日志編寫器對象。然後通過這個對象的一組方法往日志中添加事件,即生成相應的圖檔,最後啟動前端伺服器,在localhost中就可以看到最終的結果了。
訓練網絡,并可視化網絡訓練過程的代碼如下:
from tensorboardX import SummaryWriter
logger = SummaryWriter(log_dir="data/log")
# 擷取優化器和損失函數
optimizer = torch.optim.Adam(MyConvNet.parameters(), lr=3e-4)
loss_func = nn.CrossEntropyLoss()
log_step_interval = 100 # 記錄的步數間隔
for epoch in range(5):
print("epoch:", epoch)
# 每一輪都周遊一遍資料加載器
for step, (x, y) in enumerate(train_loader):
# 前向計算->計算損失函數->(從損失函數)反向傳播->更新網絡
predict = MyConvNet(x)
loss = loss_func(predict, y)
optimizer.zero_grad() # 清空梯度(可以不寫)
loss.backward() # 反向傳播計算梯度
optimizer.step() # 更新網絡
global_iter_num = epoch * len(train_loader) + step + 1 # 計算目前是從訓練開始時的第幾步(全局疊代次數)
if global_iter_num % log_step_interval == 0:
# 控制台輸出一下
print("global_step:{}, loss:{:.2}".format(global_iter_num, loss.item()))
# 添加的第一條日志:損失函數-全局疊代次數
logger.add_scalar("train loss", loss.item() ,global_step=global_iter_num)
# 在測試集上預測并計算正确率
test_predict = MyConvNet(test_data_x)
_, predict_idx = torch.max(test_predict, 1) # 計算softmax後的最大值的索引,即預測結果
acc = accuracy_score(test_data_y, predict_idx)
# 添加第二條日志:正确率-全局疊代次數
logger.add_scalar("test accuary", acc.item(), global_step=global_iter_num)
# 添加第三條日志:這個batch下的128張圖像
img = vutils.make_grid(x, nrow=12)
logger.add_image("train image sample", img, global_step=global_iter_num)
# 添加第三條日志:網絡中的參數分布直方圖
for name, param in MyConvNet.named_parameters():
logger.add_histogram(name, param.data.numpy(), global_step=global_iter_num)
運作完後,我們通過cmd來到與代碼同一級的目錄(如果你使用的是pycharm,可以通過pycharm中的終端)輸入指令
tensorboard --logdir="./data/log"
,啟動伺服器。
logdir後面的參數是日志檔案的檔案夾的路徑
然後在谷歌浏覽器中通路紅框框中的url,便可得到可視化界面,點選上面的頁面控件,可以檢視我們通過
add_scalar
、
add_image
和
add_histogram
得到的圖像,而且各方面做得都很絲滑。
以下是筆者安裝使用tensorboard時遇到的一些錯誤。
好,作為一名沒有裝過TensorFlow的windows玩家,筆者下面開始踩坑。踩完後,直接把幾個可能的錯誤呈上。
第一個錯誤,運作
tensorboard --logdir="./data/log"
,遇到報錯,内容為有重複的tensorboard的包。
解決方法:找到site-packages(如果你是像我一樣全局安裝的,那麼找到解釋器那一級目錄的site-packages,如果是在項目虛拟環境中安裝的,那麼找到項目中的site-packages),删去下圖中紅框框标出來的檔案夾。
第二個錯誤,在解決第一個錯誤後,再次運作指令,還是報錯,内容為編碼出錯。由于筆者做過一點前端,在學習webpack項目時,曾經被告知項目路徑不能含有中文,否則會有編碼錯誤,而剛才的報錯中涉及到了前端伺服器的啟動,是以,筆者想到從檔案名入手。
解決方法:確定指令涉及的檔案路徑、所有程式涉及到檔案不含中文。筆者是計算機名字含有中文,然後tensorboard的日志檔案是以本地計算機名為字尾的,是以筆者将計算機名修改成了英文,重新開機後再輸入指令就ok了。
2.2 HiddenLayer可視化訓練過程
tensorboard的圖像很華麗,但是使用過程相較于其他的工具包較為繁瑣,是以小網絡一般沒必要使用tensorboard。
import hiddenlayer as hl
import time
# 記錄訓練過程的名額
history = hl.History()
# 使用canvas進行可視化
canvas = hl.Canvas()
# 擷取優化器和損失函數
optimizer = torch.optim.Adam(MyConvNet.parameters(), lr=3e-4)
loss_func = nn.CrossEntropyLoss()
log_step_interval = 100 # 記錄的步數間隔
for epoch in range(5):
print("epoch:", epoch)
# 每一輪都周遊一遍資料加載器
for step, (x, y) in enumerate(train_loader):
# 前向計算->計算損失函數->(從損失函數)反向傳播->更新網絡
predict = MyConvNet(x)
loss = loss_func(predict, y)
optimizer.zero_grad() # 清空梯度(可以不寫)
loss.backward() # 反向傳播計算梯度
optimizer.step() # 更新網絡
global_iter_num = epoch * len(train_loader) + step + 1 # 計算目前是從訓練開始時的第幾步(全局疊代次數)
if global_iter_num % log_step_interval == 0:
# 控制台輸出一下
print("global_step:{}, loss:{:.2}".format(global_iter_num, loss.item()))
# 在測試集上預測并計算正确率
test_predict = MyConvNet(test_data_x)
_, predict_idx = torch.max(test_predict, 1) # 計算softmax後的最大值的索引,即預測結果
acc = accuracy_score(test_data_y, predict_idx)
# 以epoch和step為索引,建立日志字典
history.log((epoch, step),
train_loss=loss,
test_acc=acc,
hidden_weight=MyConvNet.fc[2].weight)
# 可視化
with canvas:
canvas.draw_plot(history["train_loss"])
canvas.draw_plot(history["test_acc"])
canvas.draw_image(history["hidden_weight"])
不同于tensorboard,hiddenlayer會在程式運作的過程中動态生成圖像,而不是模型訓練完後
下面為模型訓練的某一時刻的截圖:
三、使用Visdom進行可視化
Visdom是Facebook為pytorch開發的一塊可視化工具。類似于tensorboard,visdom也是通過在本地啟動前端伺服器來實作可視化的,而在具體操作上,visdom又類似于matplotlib.pyplot。是以使用起來很靈活。
首先先安裝visdom庫,然後補坑。由于啟動前端伺服器需要大量依賴項,是以在第一次啟動時可能會很慢(需要下載下傳前端三闆斧的依賴項),解決方法請見這裡。
先導入需要的第三方庫:
from visdom import Visdom
from sklearn.datasets import load_iris
import torch
import numpy as np
from PIL import Image
matplotlib裡,使用者繪圖可以通過plt這個對象來繪圖,在visdom中,同樣需要一個繪圖對象,我們通過
vis = Visdom()
來擷取。具體繪制時,由于我們會一次畫好幾張圖,是以visdom要求使用者在繪制時指定目前繪制圖像的視窗名字(也就是
win
這個參數);除此之外,為了到時候顯示的分塊,使用者還需要指定繪圖環境
env
,這個參數相同的圖像,最後會顯示在同一張頁面上。
繪制線圖(相當于matplotlib中的
plt.plot
)
# 繪制圖像需要的資料
iris_x, iris_y = load_iris(return_X_y=True)
# 擷取繪圖對象,相當于plt
vis = Visdom()
# 添加折線圖
x = torch.linspace(-6, 6, 100).view([-1, 1])
sigmoid = torch.nn.Sigmoid()
sigmoid_y = sigmoid(x)
tanh = torch.nn.Tanh()
tanh_y = tanh(x)
relu = torch.nn.ReLU()
relu_y = relu(x)
# 連接配接三個張量
plot_x = torch.cat([x, x, x], dim=1)
plot_y = torch.cat([sigmoid_y, tanh_y, relu_y], dim=1)
# 繪制線性圖
vis.line(X=plot_x, Y=plot_y, win="line plot", env="main",
opts={
"dash" : np.array(["solid", "dash", "dashdot"]),
"legend" : ["Sigmoid", "Tanh", "ReLU"]
})
繪制散點圖:
# 繪制2D和3D散點圖
# 參數Y用來指定點的分布,win指定圖像的視窗名稱,env指定圖像所在的環境,opts通過字典來指定一些樣式
vis.scatter(iris_x[ : , 0 : 2], Y=iris_y+1, win="windows1", env="main")
vis.scatter(iris_x[ : , 0 : 3], Y=iris_y+1, win="3D scatter", env="main",
opts={
"markersize" : 4, # 點的大小
"xlabel" : "特征1",
"ylabel" : "特征2"
})
繪制莖葉圖:
# 添加莖葉圖
x = torch.linspace(-6, 6, 100).view([-1, 1])
y1 = torch.sin(x)
y2 = torch.cos(x)
# 連接配接張量
plot_x = torch.cat([x, x], dim=1)
plot_y = torch.cat([y1, y2], dim=1)
# 繪制莖葉圖
vis.stem(X=plot_x, Y=plot_y, win="stem plot", env="main",
opts={
"legend" : ["sin", "cos"],
"title" : "莖葉圖"
})
繪制熱力圖:
# 計算鸢尾花資料集特征向量的相關系數矩陣
iris_corr = torch.from_numpy(np.corrcoef(iris_x, rowvar=False))
# 繪制熱力圖
vis.heatmap(iris_corr, win="heatmap", env="main",
opts={
"rownames" : ["x1", "x2", "x3", "x4"],
"columnnames" : ["x1", "x2", "x3", "x4"],
"title" : "熱力圖"
})
可視化圖檔,這裡我們使用自定義的env名MyPlotEnv
# 可視化圖檔
img_Image = Image.open("./example.jpg")
img_array = np.array(img_Image.convert("L"), dtype=np.float32)
img_tensor = torch.from_numpy(img_array)
print(img_tensor.shape)
# 這次env自定義
vis.image(img_tensor, win="one image", env="MyPlotEnv",
opts={
"title" : "一張圖像"
})
可視化文本,同樣在MyPlotEnv中繪制:
# 可視化文本
text = "hello world"
vis.text(text=text, win="text plot", env="MyPlotEnv",
opts={
"title" : "可視化文本"
})
運作上述代碼,再通過在終端中輸入
python3 -m visdom.server
啟動伺服器,然後根據終端傳回的URL,在谷歌浏覽器中通路這個URL,就可以看到圖像了。
在Environment中輸入不同的env參數可以看到我們在不同環境下繪制的圖檔。對于分類圖集特别有用。
在終端中按下Ctrl+C可以終止前端伺服器。
進一步
需要注意,如果你的前端伺服器停掉了,那麼所有的圖檔都會丢失,因為此時的圖像的資料都是駐留在記憶體中,而并沒有dump到本地磁盤。那麼如何儲存目前visdom中的可視化結果,并在将來複用呢?其實很簡單,比如我現在有一堆來之不易的Mel頻譜圖:
點選Manage Views
點選fork->save:(此處我隻儲存名為normal的env)
接着,在你的User目錄下(Windows是C:\Users\賬戶.visdom檔案夾,Linux是在~.visdom檔案夾下),可以看到儲存好的env:
它是以json檔案格式儲存的,那麼如果你儲存完後再shut down目前的前端伺服器,圖像資料便不會丢失。
好的,現在在儲存完你珍貴的資料後,請關閉你的visdom前端伺服器。然後再啟動它。
如何檢視儲存的資料呢?很簡答,下次打開visdom前端後,visdom會在.visdom檔案夾下讀取所有的儲存資料完成初始化,這意味着,你直接啟動visdom,其他什麼也不用做就可以看到之前儲存的資料啦!
那麼如何服用儲存的資料呢?既然你都知道了visdom儲存的資料在哪裡,那麼直接通過python的json包來讀取這個資料檔案,然後做解析就可以了,這是方法一,示範如下:
import json
with open(r"...\.visdom\normal.json", "r", encoding="utf-8") as f:
dataset : dict = json.load(f)
jsons : dict = dataset["jsons"] # 這裡存着你想要恢複的資料
reload : dict = dataset["reload"] # 這裡存着有關視窗尺寸的資料
print(jsons.keys()) # 檢視所有的win
out:
dict_keys(['jsons', 'reload'])
dict_keys(['1.wav', '2.wav', '3.wav', '4.wav', '5.wav', '6.wav', '7.wav', '8.wav', '9.wav', '10.wav', '11.wav', '12.wav', '13.wav', '14.wav'])
但這麼做不是很優雅,是以visdom封裝了第二種方法。你當然可以通過通路檔案夾.visdom來檢視目前可用的env,但是也可以這麼做:
from visdom import Visdom
vis = Visdom()
print(vis.get_env_list())
out:
Setting up a new session...
['main', 'normal']
在擷取了可用的環境名後,你可以通過get_window_data方法來擷取指定env、指定win下的圖像資料。請注意,該方法傳回str,故需要通過json來解析:
from visdom import Visdom
import json
vis = Visdom()
window = vis.get_window_data(win="1.wav", env="normal")
window = json.loads(window) # window 是 str,需要解析為字典
content = window["content"]
data = content["data"][0]
print(data.keys())
out:
Setting up a new session...
dict_keys(['z', 'x', 'y', 'zmin', 'zmax', 'type', 'colorscale'])
通過索引這些keys,相信想複用原本的圖像資料并不困難。