【深度學(xué)習(xí)】高效使用Pytorch的6個(gè)技巧:為你的訓(xùn)練Pipeline提供強(qiáng)大動(dòng)力
作者:Eugene Khvedchenya? ?編譯:ronghuaiyang
只報(bào)告模型的Top-1準(zhǔn)確率往往是不夠的。

每一個(gè)深度學(xué)習(xí)項(xiàng)目的最終目標(biāo)都是為產(chǎn)品帶來價(jià)值。當(dāng)然,我們想要最好的模型。什么是“最好的” —— 取決于特定的用例,我將把這個(gè)討論放到這篇文章之外。我想談?wù)勅绾螐哪愕?strong>train.py腳本中得到最好的模型。
在這篇文章中,我們將介紹以下技巧:
用高級框架代替自己寫的循環(huán) 使用另外的度量標(biāo)準(zhǔn)監(jiān)控訓(xùn)練的進(jìn)展 使用TensorBoard 使模型的預(yù)測可視化 使用Dict作為數(shù)據(jù)集和模型的返回值 檢測異常和解決數(shù)值的不穩(wěn)定性
免責(zé)聲明:在下一節(jié)中,我將引用一些源代碼。大多數(shù)都是為[Catalyst](https://github.com/catalysts -team/catalyst)框架(20.08版)定制的,可以在pytorch-toolbelt中使用。
不要重復(fù)造輪子

建議1 — 利用PyTorch生態(tài)系統(tǒng)的高級訓(xùn)練框架
PyTorch在從頭開始編寫訓(xùn)練循環(huán)時(shí)提供了極佳的靈活性和自由度。理論上,這為編寫任何訓(xùn)練邏輯提供了無限可能。在實(shí)踐中,你很少會(huì)為訓(xùn)練CycleGAN、distilling BERT或3D物體檢測從頭開始實(shí)現(xiàn)編寫訓(xùn)練循環(huán)。
從頭編寫一個(gè)完整的訓(xùn)練循環(huán)是學(xué)習(xí)PyTorch基本原理的一個(gè)很好的方法。不過,我強(qiáng)烈建議你在掌握了一些知識(shí)之后,轉(zhuǎn)向高級框架。有很多選擇:Catalyst, PyTorch-Lightning, Fast.AI, Ignite,以及其他。高級框架通過以下方式節(jié)省你的時(shí)間:
提供經(jīng)過良好測試的訓(xùn)練循環(huán) 支持配置文件 支持多gpu和分布式訓(xùn)練 管理檢查點(diǎn)/實(shí)驗(yàn) 自動(dòng)記錄訓(xùn)練進(jìn)度
從這些高級庫中獲得最大效果需要一些時(shí)間。然而,這種一次性的投資從長期來看是有回報(bào)的。
優(yōu)點(diǎn)
訓(xùn)練pipeline變得更小 —— 代碼越少 —— 出錯(cuò)的機(jī)會(huì)就越少。 易于進(jìn)行實(shí)驗(yàn)管理。 簡化分布式和混合精度訓(xùn)練。
缺點(diǎn)
通常,當(dāng)使用一個(gè)高級框架時(shí),我們必須在框架特定的設(shè)計(jì)原則和范例中編寫代碼。 時(shí)間投資,學(xué)習(xí)額外的框架需要時(shí)間。
給我看指標(biāo)

建議2 —— 在訓(xùn)練期間查看其他指標(biāo)
幾乎每一個(gè)用于在MNIST或CIFAR甚至ImageNet中對圖像進(jìn)行分類的快速啟動(dòng)示例項(xiàng)目都有一個(gè)共同點(diǎn) —— 它們在訓(xùn)練期間和訓(xùn)練之后都報(bào)告了一組最精簡的度量標(biāo)準(zhǔn)。通常情況下,包括Top-1和Top-5準(zhǔn)確度、錯(cuò)誤率、訓(xùn)練/驗(yàn)證損失,僅此而已。雖然這些指標(biāo)是必要的,但它只是冰山一角!
現(xiàn)代圖像分類模型有數(shù)千萬個(gè)參數(shù)。你想只使用一個(gè)標(biāo)量值來計(jì)算它嗎?
Top-1準(zhǔn)確率最好的CNN分類模型在泛化方面可能不是最好的。根據(jù)你的領(lǐng)域和需求,你可能希望保存具有最 false-positive/false-negative的模型,或者具有最高平均精度的模型。
讓我給你一些建議,在訓(xùn)練過程中你可以記錄哪些數(shù)據(jù):
Grad-CAM heat-map —— 看看圖像的哪個(gè)部分對某一特定類的貢獻(xiàn)最大。

Confusion Matrix — 顯示了對你的模型來說哪兩個(gè)類最具挑戰(zhàn)性。

Distribution of predictions — 讓你了解最優(yōu)決策邊界。

Minimum/Average/Maximum 跨所有層的梯度值,允許識(shí)別是否在模型中存在消失/爆炸的梯度或初始化不好的層。
使用面板工具來監(jiān)控訓(xùn)練
建議3 — 使用TensorBoard或任何其他解決方案來監(jiān)控訓(xùn)練進(jìn)度
在訓(xùn)練模型時(shí),你可能最不愿意做的事情就是查看控制臺(tái)輸出。通過一個(gè)功能強(qiáng)大的儀表板,你可以在其中一次看到所有的度量標(biāo)準(zhǔn),這是檢查訓(xùn)練結(jié)果的更有效的方法。

對于少量實(shí)驗(yàn)和非分布式環(huán)境,TensorBoard是一個(gè)黃金標(biāo)準(zhǔn)。自版本1.3以來,PyTorch就完全支持它,并提供了一組豐富的特性來管理試用版。還有一些更先進(jìn)的基于云的解決方案,比如Weights&Biases、[Alchemy](https://github.com/catalyst team/alchemy)和TensorBoard.dev,這些解決方案使得在多臺(tái)機(jī)器上監(jiān)控和比較訓(xùn)練變得更容易。
當(dāng)使用Tensorboard時(shí),我通常記錄這樣一組指標(biāo):
學(xué)習(xí)率和其他可能改變的優(yōu)化參數(shù)(動(dòng)量,重量衰減,等等) 用于數(shù)據(jù)預(yù)處理和模型內(nèi)部的時(shí)間 貫穿訓(xùn)練和驗(yàn)證的損失(每個(gè)batch和每個(gè)epoch的平均值) 跨訓(xùn)練和驗(yàn)證的度量 訓(xùn)練session的超參數(shù)最終值 混淆矩陣,Precision-Recall曲線,AUC(如果適用) 模型預(yù)測的可視化(如適用)
一圖勝千言
直觀地觀察模型的預(yù)測是非常重要的。有時(shí)訓(xùn)練數(shù)據(jù)是有噪聲的;有時(shí),模型會(huì)過擬合圖像的偽影。通過可視化最好的和最差的batch(基于損失或你感興趣的度量),你可以對模型執(zhí)行良好和糟糕的情況進(jìn)行有價(jià)值的洞察。
建議5 — 可視化每個(gè)epoch中最好和最壞的batch。它可能會(huì)給你寶貴的見解。
Catalyst用戶提示:這里是使用可視化回調(diào)的示例:https://github.com/BloodAxe/Catalyst-Inria-Segmentation-Example/blob/master/fit_predict.py#L258
例如,在全球小麥檢測挑戰(zhàn)中,我們需要在圖像上檢測小麥頭。通過可視化最佳batch的圖片(基于mAP度量),我們看到模型在尋找小物體方面做得近乎完美。

相反,當(dāng)我們查看最差一批的第一個(gè)樣本時(shí),我們看到模型很難對大物體做出準(zhǔn)確的預(yù)測??梢暬治鰹槿魏螖?shù)據(jù)科學(xué)家都提供了寶貴的見解。

查看最差的batch也有助于發(fā)現(xiàn)數(shù)據(jù)標(biāo)記中的錯(cuò)誤。通常情況下,貼錯(cuò)標(biāo)簽的樣本損失更大,因此會(huì)成為最差的batch。通過在每個(gè)epoch對最糟糕的batch做一個(gè)視覺檢查,你可以消除這些錯(cuò)誤:

使用Dict作為Dataset和Model的返回值
建議4 — 如果你的模型返回一個(gè)以上的值,使用
Dict來返回結(jié)果,不要使用tuple
在復(fù)雜的模型中,返回多個(gè)輸出并不少見。例如,目標(biāo)檢測模型通常返回邊界框及其標(biāo)簽,在圖像分割CNN-s中,我們經(jīng)常返回中間層的mask進(jìn)行深度監(jiān)督,多任務(wù)學(xué)習(xí)最近也很常用。
在許多開源實(shí)現(xiàn)中,我經(jīng)??吹竭@樣的東西:
#?Bad?practice,?don't?return?tuple
class?RetinaNet(nn.Module):
??...
??def?forward(self,?image):
????x?=?self.encoder(image)
????x?=?self.decoder(x)
????bboxes,?scores?=?self.head(x)
????return?bboxes,?scores
??...
對于作者來說,我認(rèn)為這是一種非常糟糕的從模型返回結(jié)果的方法。下面是我推薦的替代方法:
class?RetinaNet(nn.Module):
??RETINA_NET_OUTPUT_BBOXES?=?"bboxes"
??RETINA_NET_OUTPUT_SCORES?=?"scores"
??...
??def?forward(self,?image):
????x?=?self.encoder(image)
????x?=?self.decoder(x)
????bboxes,?scores?=?self.head(x)
????return?{?RETINA_NET_OUTPUT_BBOXES:?bboxes,?
?????????????RETINA_NET_OUTPUT_SCORES:?scores?}
??...
這個(gè)建議在某種程度上與“The Zen of Python”的設(shè)定產(chǎn)生了共鳴 —— “明確的比含蓄的更好”。遵循這一規(guī)則將使你的代碼更清晰、更容易維護(hù)。
那么為什么我認(rèn)為第二種選擇更好呢?有幾個(gè)原因:
返回值有一個(gè)顯式的名稱與它關(guān)聯(lián)。你不需要記住元組中元素的確切順序。 如果你需要訪問返回的字典的一個(gè)特定元素,你可以通過它的名字來訪問。 從模型中添加新的輸出不會(huì)破壞代碼。
使用Dict,你甚至可以更改模型的行為,以按需返回額外的輸出。例如,這里有一個(gè)簡短的片段,演示了如何返回多個(gè)“主”輸出和兩個(gè)“輔助”輸出來進(jìn)行度量學(xué)習(xí):
#?https://github.com/BloodAxe/Kaggle-2020-Alaska2/blob/master/alaska2/models/timm.py#L104
def?forward(self,?**kwargs):
??x?=?kwargs[self.input_key]
??x?=?self.rgb_bn(x)
??x?=?self.encoder.forward_features(x)
??embedding?=?self.pool(x)
??result?=?{
????OUTPUT_PRED_MODIFICATION_FLAG:?self.flag_classifier(self.drop(embedding)),
????OUTPUT_PRED_MODIFICATION_TYPE:?self.type_classifier(self.drop(embedding)),
??}
??if?self.need_embedding:
????result[OUTPUT_PRED_EMBEDDING]?=?embedding
??if?self.arc_margin?is?not?None:
????result[OUTPUT_PRED_EMBEDDING_ARC_MARGIN]?=?self.arc_margin(embedding)
??return?result
同樣的建議也適用于Dataset類。對于Cifar-10玩具示例,可以將圖像及其對應(yīng)的標(biāo)簽作為元組返回。但當(dāng)處理多任務(wù)或多輸入模型,你想從數(shù)據(jù)集返回Dict類型的樣本:
#?https://github.com/BloodAxe/Kaggle-2020-Alaska2/blob/master/alaska2/dataset.py#L373
class?TrainingValidationDataset(Dataset):
????def?__init__(
????????self,
????????images:?Union[List,?np.ndarray],
????????targets:?Optional[Union[List,?np.ndarray]],
????????quality:?Union[List,?np.ndarray],
????????bits:?Optional[Union[List,?np.ndarray]],
????????transform:?Union[A.Compose,?A.BasicTransform],
????????features:?List[str],
????):
????????"""
????????:param?obliterate?-?Augmentation?that?destroys?embedding.
????????"""
????????if?targets?is?not?None:
????????????if?len(images)?!=?len(targets):
????????????????raise?ValueError(f"Size?of?images?and?targets?does?not?match:?{len(images)}?{len(targets)}")
????????self.images?=?images
????????self.targets?=?targets
????????self.transform?=?transform
????????self.features?=?features
????????self.quality?=?quality
????????self.bits?=?bits
????def?__len__(self):
????????return?len(self.images)
????def?__repr__(self):
????????return?f"TrainingValidationDataset(len={len(self)},?targets_hist={np.bincount(self.targets)},?qf={np.bincount(self.quality)},?features={self.features})"
????def?__getitem__(self,?index):
????????image_fname?=?self.images[index]
????????try:
????????????image?=?cv2.imread(image_fname)
????????????if?image?is?None:
????????????????raise?FileNotFoundError(image_fname)
????????except?Exception?as?e:
????????????print("Cannot?read?image?",?image_fname,?"at?index",?index)
????????????print(e)
????????qf?=?self.quality[index]
????????data?=?{}
????????data["image"]?=?image
????????data.update(compute_features(image,?image_fname,?self.features))
????????data?=?self.transform(**data)
????????sample?=?{INPUT_IMAGE_ID_KEY:?os.path.basename(self.images[index]),?INPUT_IMAGE_QF_KEY:?int(qf)}
????????if?self.bits?is?not?None:
????????????#?OK
????????????sample[INPUT_TRUE_PAYLOAD_BITS]?=?torch.tensor(self.bits[index],?dtype=torch.float32)
????????if?self.targets?is?not?None:
????????????target?=?int(self.targets[index])
????????????sample[INPUT_TRUE_MODIFICATION_TYPE]?=?target
????????????sample[INPUT_TRUE_MODIFICATION_FLAG]?=?torch.tensor([target?>?0]).float()
????????for?key,?value?in?data.items():
????????????if?key?in?self.features:
????????????????sample[key]?=?tensor_from_rgb_image(value)
????????return?sample
當(dāng)你的代碼中有Dictionaries時(shí),你可以在任何地方使用名稱常量引用輸入/輸出。遵循這條規(guī)則將使你的訓(xùn)練管道非常清晰和容易遵循:
#?https://github.com/BloodAxe/Kaggle-2020-Alaska2
callbacks?+=?[
??CriterionCallback(
????input_key=INPUT_TRUE_MODIFICATION_FLAG,
????output_key=OUTPUT_PRED_MODIFICATION_FLAG,
????criterion_key="bce"
??),
??CriterionCallback(
????input_key=INPUT_TRUE_MODIFICATION_TYPE,
????output_key=OUTPUT_PRED_MODIFICATION_TYPE,
????criterion_key="ce"
??),
??CompetitionMetricCallback(
????input_key=INPUT_TRUE_MODIFICATION_FLAG,
????output_key=OUTPUT_PRED_MODIFICATION_FLAG,
????prefix="auc",
????output_activation=binary_logits_to_probas,
????class_names=class_names,
??),
??OutputDistributionCallback(
??????input_key=INPUT_TRUE_MODIFICATION_FLAG,
??????output_key=OUTPUT_PRED_MODIFICATION_FLAG,
??????output_activation=binary_logits_to_probas,
??????prefix="distribution/binary",
??),
??BestMetricCheckpointCallback(
????target_metric="auc",?
????target_metric_minimize=False,?
????save_n_best=3),
]
在訓(xùn)練中檢測異常

建議5 — 在訓(xùn)練期間使用
torch.autograd.detect_anomaly()查找算術(shù)異常
如果你在訓(xùn)練過程中在損失/度量中看到NaNs或Inf,你的腦海中就會(huì)響起一個(gè)警報(bào)。它是你的管道中有問題的指示器。通常情況下,它可能由以下原因引起:
模型或特定層的初始化不好(你可以通過觀察梯度大小來檢查哪些層) 數(shù)學(xué)上不正確的運(yùn)算(負(fù)數(shù)的 torch.sqrt(),非正數(shù)的torch.log(),等等)不當(dāng)使用 torch.mean()和torch.sum()的reduction(zero-sized張量上的均值會(huì)得到nan,大張量上的sum容易導(dǎo)致溢出)在loss中使用 x.sigmoid()(如果你需要在loss函數(shù)中使用概率,更好的方法是x.sigmoid().clamp(eps,1-eps)以防止梯度消失)在Adam-like的優(yōu)化器中的低epsilon值 在使用fp16的訓(xùn)練的時(shí)候沒有使用動(dòng)態(tài)損失縮放
為了找到你代碼中第一次出現(xiàn)Nan/Inf的確切位置,PyTorch提供了一個(gè)簡單易用的方法torch. autograde .detect_anomaly():
import?torch
def?main():
????torch.autograd.detect_anomaly()
????...
????#?Rest?of?the?training?code
???
#?OR
class?MyNumericallyUnstableLoss(nn.Module):
??def?forward(self,?input,?target):
????with?torch.autograd.set_detect_anomaly(True):
???????loss?=?input?*?target
???????return?loss
將其用于調(diào)試目的,否則就禁用它,異常檢測會(huì)帶來計(jì)算開銷,并將訓(xùn)練速度降低10-15% 。

英文原文:https://towardsdatascience.com/efficient-pytorch-supercharging-training-pipeline-19a26265adae
往期精彩回顧
獲取一折本站知識(shí)星球優(yōu)惠券,復(fù)制鏈接直接打開:
https://t.zsxq.com/662nyZF
本站qq群1003271085。
加入微信群請掃碼進(jìn)群(如果是博士或者準(zhǔn)備讀博士請說明):
