0803-PyTorch的Debug指南
pytorch完整教程目錄:https://www.cnblogs.com/nickchen121/p/14662511.html
一、ipdb 介紹
很多初學 python 的同學會使用 print 或 log 除錯程式,但是這隻在小規模的程式下除錯很方便,更好的除錯應該是在一邊執行的時候一邊檢查裡面的變數和方法。
感興趣的可以去了解 pycharm 的 debug 模式,功能也很強大,能夠滿足一般的需求,這裡不多做贅述,我們這裡介紹一個更適用於 pytorch 的一個靈活的 pdb 互動式除錯工具。
Pdb 是一個互動式的除錯工具,整合與 Python 標準庫中,它能讓你根據需求跳轉到任意的 Python 程式碼斷點、檢視任意變數、單步執行程式碼,甚至還能修改變數的值,而沒有必要去重啟程式。
ipdb 則是一個增強版的 pdb,它提供了除錯模式下的程式碼自動補全,還有更好的語法高亮和程式碼溯源,以及更好的內省功能,最重要的是它和 pdb 介面完全相容,可以通過 pip install ipdb
安裝。
二、ipdb 的使用
首先看一個例子,要使用 ipdb 的話,只需要在想要進行除錯的地方插入 ipdb.set_trace()
,當程式碼執行到這個地方時,就會自動進入互動式除錯模式。
import ipdb
def sum(x):
r = 0
for ii in x:
r += ii
return r
def mul(x):
r = 1
for ii in x:
r *= 11
return r
ipdb.set_trace()
x = [1, 2, 3, 4, 5]
r = sum(x)
r = mul(x)
> /Users/mac/Desktop/jupyter/test.py(19)<module>()
18 ipdb.set_trace()
---> 19 x = [1, 2, 3, 4, 5]
20 r = sum(x)
ipdb> l 1,5 # l(ist) 1,5 的縮寫,檢視第 1 行到第 5 行的程式碼
1 import ipdb
2
3
4 def sum(x):
5 r = 0
ipdb> n # n(ext) 的縮寫執行下一步
> /Users/mac/Desktop/jupyter/test.py(20)<module>()
19 x = [1, 2, 3, 4, 5]
---> 20 r = sum(x)
21 r = mul(x)
ipdb> s # s(tep) 的縮寫,進入 sum 函式內部
--Call--
> /Users/mac/Desktop/jupyter/test.py(4)sum()
3
----> 4 def sum(x):
5 r = 0
ipdb> n # n(ext) 單步執行
> /Users/mac/Desktop/jupyter/test.py(5)sum()
4 def sum(x):
----> 5 r = 0
6 for ii in x:
ipdb> n
> /Users/mac/Desktop/jupyter/test.py(6)sum()
5 r = 0
----> 6 for ii in x:
7 r += ii
ipdb> u # u(p) 的縮寫,調回上一層的呼叫
> /Users/mac/Desktop/jupyter/test.py(20)<module>()
19 x = [1, 2, 3, 4, 5]
---> 20 r = sum(x)
21 r = mul(x)
ipdb> d # d(own) 的縮寫,跳到呼叫的下一層
> /Users/mac/Desktop/jupyter/test.py(6)sum()
5 r = 0
----> 6 for ii in x:
7 r += ii
ipdb> n
> /Users/mac/Desktop/jupyter/test.py(7)sum()
6 for ii in x:
----> 7 r += ii
8 return r
ipdb> !r # 檢視變數 r 的值,該變數名與除錯命令 `r(eturn)` 衝突
0
ipdb> return # 繼續執行知道函式返回
--Return--
15
> /Users/mac/Desktop/jupyter/test.py(8)sum()
7 r += ii
----> 8 return r
9
ipdb> n
> /Users/mac/Desktop/jupyter/test.py(21)<module>()
19 x = [1, 2, 3, 4, 5]
20 r = sum(x)
---> 21 r = mul(x)
ipdb> x # 檢視變數 x
[1, 2, 3, 4, 5]
ipdb> x[0] = 10000 # 修改變數 x
ipdb> x
[10000, 2, 3, 4, 5]
ipdb> b 12 # b(reak) 的縮寫,在第 10 行設定斷點
Breakpoint 1 at /Users/mac/Desktop/jupyter/test.py:12
ipdb> c # c(ontinue) 的縮寫,繼續執行,直到遇到斷點
> /Users/mac/Desktop/jupyter/test.py(12)mul()
11 def mul(x):
1--> 12 r = 1
13 for ii in x:
ipdb> return # 可以看到計算的是修改之後的 x 的乘積
--Return--
1200000
> /Users/mac/Desktop/jupyter/test.py(15)mul()
14 r *= ii
---> 15 return r
16
ipdb> q # q(uit) 的縮寫,退出 debug
上述只是給出了 ipdb 的一部分使用方法,關於 ipdb 還有一些小的使用技巧:
鍵能夠自動補齊,補齊用法和 IPython 中的類似 - j(ump)
能夠跳過中間某些行的程式碼的執行 - 可以直接在 ipdb 中修改變數的值
- help 能夠檢視除錯命令的用法,比如 h h 可以檢視 help 命令的用法,h j(ump) 能夠檢視 j(ump) 命令的用法
三、在 PyTorch 中 Debug
PyTorch 作為一個動態圖框架,和 ipdb 結合使用能夠讓除錯過程更加便捷,下面我們將距離說明以下三點:
- 如何在 PyTorch 中檢視神經網路各個層的輸出
- 如何在 PyTorch 中分析各個引數的梯度
- 如何動態修改 PyTorch 的訓練流程
首先,執行上一篇文章給出的“貓狗大戰”程式:python main.py train --debug-file='debug/debug.txt'
程式執行一段時間後,在debug目錄下建立debug.txt標識檔案,當程式檢測到這個檔案存在時,會自動進入debug模式。
99it [00:17, 6.07it/s]loss: 0.22854854568839075
119it [00:21, 5.79it/s]loss: 0.21267264398435753
139it [00:24, 5.99it/s]loss: 0.19839374726372108
> e:/Users/mac/Desktop/jupyter/mdFile/deeplearning/main.py(80)train()
79 loss_meter.reset()
---> 80 confusion_matrix.reset()
81 for ii, (data, label) in tqdm(enumerate(train_dataloader)):
ipdb> break 88 # 在第88行設定斷點,當程式執行到此處進入debug模式
Breakpoint 1 at e:/Users/mac/Desktop/jupyter/mdFile/deeplearning/main.py:88
ipdb> # 列印所有引數及其梯度的標準差
for (name,p) in model.named_parameters(): \
print(name,p.data.std(),p.grad.data.std())
model.features.0.weight tensor(0.2615, device='cuda:0') tensor(0.3769, device='cuda:0')
model.features.0.bias tensor(0.4862, device='cuda:0') tensor(0.3368, device='cuda:0')
model.features.3.squeeze.weight tensor(0.2738, device='cuda:0') tensor(0.3023, device='cuda:0')
model.features.3.squeeze.bias tensor(0.5867, device='cuda:0') tensor(0.3753, device='cuda:0')
model.features.3.expand1x1.weight tensor(0.2168, device='cuda:0') tensor(0.2883, device='cuda:0')
model.features.3.expand1x1.bias tensor(0.2256, device='cuda:0') tensor(0.1147, device='cuda:0')
model.features.3.expand3x3.weight tensor(0.0935, device='cuda:0') tensor(0.1605, device='cuda:0')
model.features.3.expand3x3.bias tensor(0.1421, device='cuda:0') tensor(0.0583, device='cuda:0')
model.features.4.squeeze.weight tensor(0.1976, device='cuda:0') tensor(0.2137, device='cuda:0')
model.features.4.squeeze.bias tensor(0.4058, device='cuda:0') tensor(0.1798, device='cuda:0')
model.features.4.expand1x1.weight tensor(0.2144, device='cuda:0') tensor(0.4214, device='cuda:0')
model.features.4.expand1x1.bias tensor(0.4994, device='cuda:0') tensor(0.0958, device='cuda:0')
model.features.4.expand3x3.weight tensor(0.1063, device='cuda:0') tensor(0.2963, device='cuda:0')
model.features.4.expand3x3.bias tensor(0.0489, device='cuda:0') tensor(0.0719, device='cuda:0')
model.features.6.squeeze.weight tensor(0.1736, device='cuda:0') tensor(0.3544, device='cuda:0')
model.features.6.squeeze.bias tensor(0.2420, device='cuda:0') tensor(0.0896, device='cuda:0')
model.features.6.expand1x1.weight tensor(0.1211, device='cuda:0') tensor(0.2428, device='cuda:0')
model.features.6.expand1x1.bias tensor(0.0670, device='cuda:0') tensor(0.0162, device='cuda:0')
model.features.6.expand3x3.weight tensor(0.0593, device='cuda:0') tensor(0.1917, device='cuda:0')
model.features.6.expand3x3.bias tensor(0.0227, device='cuda:0') tensor(0.0160, device='cuda:0')
model.features.7.squeeze.weight tensor(0.1207, device='cuda:0') tensor(0.2179, device='cuda:0')
model.features.7.squeeze.bias tensor(0.1484, device='cuda:0') tensor(0.0381, device='cuda:0')
model.features.7.expand1x1.weight tensor(0.1235, device='cuda:0') tensor(0.2279, device='cuda:0')
model.features.7.expand1x1.bias tensor(0.0450, device='cuda:0') tensor(0.0100, device='cuda:0')
model.features.7.expand3x3.weight tensor(0.0609, device='cuda:0') tensor(0.1628, device='cuda:0')
model.features.7.expand3x3.bias tensor(0.0132, device='cuda:0') tensor(0.0079, device='cuda:0')
model.features.9.squeeze.weight tensor(0.1093, device='cuda:0') tensor(0.2459, device='cuda:0')
model.features.9.squeeze.bias tensor(0.0646, device='cuda:0') tensor(0.0135, device='cuda:0')
model.features.9.expand1x1.weight tensor(0.0840, device='cuda:0') tensor(0.1860, device='cuda:0')
model.features.9.expand1x1.bias tensor(0.0177, device='cuda:0') tensor(0.0033, device='cuda:0')
model.features.9.expand3x3.weight tensor(0.0476, device='cuda:0') tensor(0.1393, device='cuda:0')
model.features.9.expand3x3.bias tensor(0.0058, device='cuda:0') tensor(0.0030, device='cuda:0')
model.features.10.squeeze.weight tensor(0.0872, device='cuda:0') tensor(0.1676, device='cuda:0')
model.features.10.squeeze.bias tensor(0.0484, device='cuda:0') tensor(0.0088, device='cuda:0')
model.features.10.expand1x1.weight tensor(0.0859, device='cuda:0') tensor(0.2145, device='cuda:0')
model.features.10.expand1x1.bias tensor(0.0160, device='cuda:0') tensor(0.0025, device='cuda:0')
model.features.10.expand3x3.weight tensor(0.0456, device='cuda:0') tensor(0.1429, device='cuda:0')
model.features.10.expand3x3.bias tensor(0.0070, device='cuda:0') tensor(0.0021, device='cuda:0')
model.features.11.squeeze.weight tensor(0.0786, device='cuda:0') tensor(0.2003, device='cuda:0')
model.features.11.squeeze.bias tensor(0.0422, device='cuda:0') tensor(0.0069, device='cuda:0')
model.features.11.expand1x1.weight tensor(0.0690, device='cuda:0') tensor(0.1400, device='cuda:0')
model.features.11.expand1x1.bias tensor(0.0138, device='cuda:0') tensor(0.0022, device='cuda:0')
model.features.11.expand3x3.weight tensor(0.0366, device='cuda:0') tensor(0.1517, device='cuda:0')
model.features.11.expand3x3.bias tensor(0.0109, device='cuda:0') tensor(0.0023, device='cuda:0')
model.features.12.squeeze.weight tensor(0.0729, device='cuda:0') tensor(0.1736, device='cuda:0')
model.features.12.squeeze.bias tensor(0.0814, device='cuda:0') tensor(0.0084, device='cuda:0')
model.features.12.expand1x1.weight tensor(0.0977, device='cuda:0') tensor(0.1385, device='cuda:0')
model.features.12.expand1x1.bias tensor(0.0102, device='cuda:0') tensor(0.0032, device='cuda:0')
model.features.12.expand3x3.weight tensor(0.0365, device='cuda:0') tensor(0.1312, device='cuda:0')
model.features.12.expand3x3.bias tensor(0.0038, device='cuda:0') tensor(0.0026, device='cuda:0')
model.classifier.1.weight tensor(0.0285, device='cuda:0') tensor(0.0865, device='cuda:0')
model.classifier.1.bias tensor(0.0362, device='cuda:0') tensor(0.0192, device='cuda:0')
ipdb> opt.lr # 檢視學習率
0.001
ipdb> opt.lr = 0.002 # 更改學習率
ipdb> for p in optimizer.param_groups: \
p['lr'] = opt.lr
ipdb> model.save() # 儲存模型
'checkpoints/squeezenet_20191004212249.pth'
ipdb> c # 繼續執行,直到第88行暫停
222it [16:38, 35.62s/it]> e:/Users/mac/Desktop/jupyter/mdFile/deeplearning/main.py(88)train()
87 optimizer.zero_grad()
1--> 88 score = model(input)
89 loss = criterion(score, target)
ipdb> s # 進入model(input)內部,即model.__call__(input)
--Call--
> c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\module.py(537)__call__()
536
--> 537 def __call__(self, *input, **kwargs):
538 for hook in self._forward_pre_hooks.values():
ipdb> n # 下一步
> c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\module.py(538)__call__()
537 def __call__(self, *input, **kwargs):
--> 538 for hook in self._forward_pre_hooks.values():
539 result = hook(self, input)
ipdb> n # 下一步
> c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\module.py(544)__call__()
543 input = result
--> 544 if torch._C._get_tracing_state():
545 result = self._slow_forward(*input, **kwargs)
ipdb> n # 下一步
> c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\module.py(547)__call__()
546 else:
--> 547 result = self.forward(*input, **kwargs)
548 for hook in self._forward_hooks.values():
ipdb> s # 進入forward函式內容
--Call--
> c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\loss.py(914)forward()
913
--> 914 def forward(self, input, target):
915 return F.cross_entropy(input, target, weight=self.weight,
ipdb> input # 檢視input變數值
tensor([[4.5005, 2.0725],
[3.5933, 7.8643],
[2.9086, 3.4209],
[2.7740, 4.4332],
[6.0164, 2.3033],
[5.2261, 3.2189],
[2.6529, 2.0749],
[6.3259, 2.2383],
[3.0629, 3.4832],
[2.7008, 8.2818],
[5.5684, 2.1567],
[3.0689, 6.1022],
[3.4848, 5.3831],
[1.7920, 5.7709],
[6.5032, 2.8080],
[2.3071, 5.2417],
[3.7474, 5.0263],
[4.3682, 3.6707],
[2.2196, 6.9298],
[5.2201, 2.3034],
[6.4315, 1.4970],
[3.4684, 4.0371],
[3.9620, 1.7629],
[1.7069, 7.8898],
[3.0462, 1.6505],
[2.4081, 6.4456],
[2.1932, 7.4614],
[2.3405, 2.7603],
[1.9478, 8.4156],
[2.7935, 7.8331],
[1.8898, 3.8836],
[3.3008, 1.6832]], device='cuda:0', grad_fn=<AsStridedBackward>)
ipdb> input.data.mean() # 檢視input的均值和標準差
tensor(3.9630, device='cuda:0')
ipdb> input.data.std()
tensor(1.9513, device='cuda:0')
ipdb> u # 跳回上一層
> c:\programdata\anaconda3\lib\site-packages\torch\nn\modules\module.py(547)__call__()
546 else:
--> 547 result = self.forward(*input, **kwargs)
548 for hook in self._forward_hooks.values():
ipdb> u # 跳回上一層
> e:/Users/mac/Desktop/jupyter/mdFile/deeplearning/main.py(88)train()
87 optimizer.zero_grad()
1--> 88 score = model(input)
89 loss = criterion(score, target)
ipdb> clear # 清除所有斷點
Clear all breaks? y
Deleted breakpoint 1 at e:/Users/mac/Desktop/jupyter/mdFile/deeplearning/main.py:88
ipdb> c # 繼續執行,記得先刪除"debug/debug.txt",否則很快又會進入除錯模式
59it [06:21, 5.75it/s]loss: 0.24856307208538073
76it [06:24, 5.91it/s]
當我們想要進入 debug 模式,修改程式中某些引數值或者想分析程式時,就可以通過建立 debug 標識檔案,此時程式會進入除錯模式,除錯完成之後刪除這個檔案並在 ipdb 除錯介面輸入 c 繼續執行程式。如果想退出程式,也可以使用這種方式,先建立 debug 標識檔案,然後輸入 quit 在退出 debug 的同時退出程式。這種退出程式的方式,與使用 Ctrl + C 的方式相比更安全,因為這能保證資料載入的多程式程式也能正確地退出,並釋放記憶體、視訊記憶體等資源。
PyTorch 和 ipdb 集合能完成很多其他框架所不能完成或很難完成的功能。根據筆者日常使用的總結,主要有以下幾個部分:
- 通過 debug 暫停程式。當程式進入 debug 模式後,將不再執行 CPU 和 GPU 運算,但是記憶體和視訊記憶體及相應的堆疊空間不會釋放。
- 通過 debug 分析程式,檢視每個層的輸出,檢視網路的引數情況。通過 u(p) 、 d(own) 、 s(tep) 等命令,能夠進入指定的程式碼,通過 n(ext) 可以單步執行,從而看到每一層的運算結果,便於分析網路的數值分佈等資訊。
- 作為動態圖框架, PyTorch 擁有 Python 動態語言解釋執行的優點,我們能夠在執行程式時,用過 ipdb 修改某些變數的值或屬性,這些修改能夠立即生效。例如可以在訓練開始不久根據損失函式調整學習率,不必重啟程式。
- 如果在 IPython 中通過 %run 魔法方法執行程式,那麼在程式異常退出時,可以使用 %debug 命令,直接進入 debug 模式,通過 u(p) 和 d(own) 跳到報錯的地方,檢視對應的變數,找出原因後修改相應的程式碼即可。有時我們的模式訓練了好幾個小時,卻在將要儲存模式之前,因為一個小小的拼寫錯誤異常退出。此時,如果修改錯誤再重新執行程式又要花費好幾個小時,太浪費時間。因此最好的方法就是看利用 %debug 進入除錯模式,在除錯模式中直接執行 model . save() 儲存模型。在 IPython 中, %pdb 魔術方法能夠使得程式出現問題後,不用手動輸入 %debug 而自動進入 debug 模式,建議使用。
四、 通過PyTorch實現專案中容易遇到的問題
PyTorch 呼叫 CuDNN 報錯時,報錯資訊諸如 CUDNN_STATUS_BAD_PARAM
,從這些報錯內容很難得到有用的幫助資訊,最後先利用 PCU 執行程式碼,此時一般會得到相對友好的報錯資訊,例如在 ipdb 中執行 model.cpu() (input.cpu())
, PyTorch 底層的 TH 庫會給出相對比較詳細的資訊。
常見的錯誤主要有以下幾種:
- 型別不匹配問題。例如 CrossEntropyLoss 的輸入 target 應該是一個 LongTensor ,而很多人輸入 FloatTensor 。
- 部分資料忘記從 CPU 轉移到 GPU 。例如,當 model 存放於 GPU 時,輸入 input 也需要轉移到 GPU 才能輸入到 model 中。還有可能就是把多個 model 存放於一個 list 物件,而在執行
model.cuda()
時,這個 list 中的物件是不會被轉移到 CUDA 上的,正確的用法是用 ModuleList 代替。 - Tensor 形狀不匹配。此類問題一般是輸入資料形狀不對,或是網路結構設計有問題,一般通過 u(p) 跳到指定程式碼,檢視輸入和模型引數的形狀即可得知。
此外,可能還會經常遇到程式正常執行、沒有報錯,但是模型無法收斂的問題。例如對於二分類問題,交叉熵損失一直徘徊在 0.69 附近(ln2),或者是數值出現溢位等問題,此時可以進入 debug 模式,用單步執行檢視,每一層輸出的均值和方差,觀察從哪一層的輸出開始出現數值異常。還要檢視每個引數梯度的均值和方差,檢視是否出現梯度消失或者梯度爆炸等問題。一般來說,通過再啟用函式之前增加 BatchNorm 層、合理的引數初始化、使用 Adam 優化器、學習率設為0.001,基本就能確保模型在一定程度收斂。
五、第八章總結
本章帶同學們從頭實現了一個 Kaggle 上的經典競賽,重點講解了如何合理地組合安排程式,同時介紹了一些在PyTorch中除錯的技巧,下章將正式的進入程式設計實戰之旅,其中一些細節不會再講的如此詳細,做好心理準備。