点击上方“AI公园”,关注公众号,选择加“星标“或“置顶”git
做者:Eugene Khvedchenyagithub
编译:ronghuaiyang
web
只报告模型的Top-1准确率每每是不够的。微信
每个深度学习项目的最终目标都是为产品带来价值。固然,咱们想要最好的模型。什么是“最好的” —— 取决于特定的用例,我将把这个讨论放到这篇文章以外。我想谈谈如何从你的train.py脚本中获得最好的模型。session
在这篇文章中,咱们将介绍如下技巧:
-
用高级框架代替本身写的循环 -
使用另外的度量标准监控训练的进展 -
使用TensorBoard -
使模型的预测可视化 -
使用Dict做为数据集和模型的返回值 -
检测异常和解决数值的不稳定性
免责声明:在下一节中,我将引用一些源代码。大多数都是为[Catalyst](https://github.com/catalysts -team/catalyst)框架(20.08版)定制的,能够在pytorch-toolbelt中使用。框架
不要重复造轮子
建议1 — 利用PyTorch生态系统的高级训练框架编辑器
PyTorch在从头开始编写训练循环时提供了极佳的灵活性和自由度。理论上,这为编写任何训练逻辑提供了无限可能。在实践中,你不多会为训练CycleGAN、distilling BERT或3D物体检测从头开始实现编写训练循环。分布式
从头编写一个完整的训练循环是学习PyTorch基本原理的一个很好的方法。不过,我强烈建议你在掌握了一些知识以后,转向高级框架。有不少选择:Catalyst, PyTorch-Lightning, Fast.AI, Ignite,以及其余。高级框架经过如下方式节省你的时间:函数
-
提供通过良好测试的训练循环 -
支持配置文件 -
支持多gpu和分布式训练 -
管理检查点/实验 -
自动记录训练进度
从这些高级库中得到最大效果须要一些时间。然而,这种一次性的投资从长期来看是有回报的。工具
优势
-
训练pipeline变得更小 —— 代码越少 —— 出错的机会就越少。 -
易于进行实验管理。 -
简化分布式和混合精度训练。
缺点
-
一般,当使用一个高级框架时,咱们必须在框架特定的设计原则和范例中编写代码。 -
时间投资,学习额外的框架须要时间。
给我看指标
建议2 —— 在训练期间查看其余指标
几乎每个用于在MNIST或CIFAR甚至ImageNet中对图像进行分类的快速启动示例项目都有一个共同点 —— 它们在训练期间和训练以后都报告了一组最精简的度量标准。一般状况下,包括Top-1和Top-5准确度、错误率、训练/验证损失,仅此而已。虽然这些指标是必要的,但它只是冰山一角!
现代图像分类模型有数千万个参数。你想只使用一个标量值来计算它吗?
Top-1准确率最好的CNN分类模型在泛化方面可能不是最好的。根据你的领域和需求,你可能但愿保存具备最 false-positive/false-negative的模型,或者具备最高平均精度的模型。
让我给你一些建议,在训练过程当中你能够记录哪些数据:
-
Grad-CAM heat-map —— 看看图像的哪一个部分对某一特定类的贡献最大。
-
Confusion Matrix — 显示了对你的模型来讲哪两个类最具挑战性。
-
Distribution of predictions — 让你了解最优决策边界。
-
Minimum/Average/Maximum 跨全部层的梯度值,容许识别是否在模型中存在消失/爆炸的梯度或初始化很差的层。
使用面板工具来监控训练
建议3 — 使用TensorBoard或任何其余解决方案来监控训练进度
在训练模型时,你可能最不肯意作的事情就是查看控制台输出。经过一个功能强大的仪表板,你能够在其中一次看到全部的度量标准,这是检查训练结果的更有效的方法。
对于少许实验和非分布式环境,TensorBoard是一个黄金标准。自版本1.3以来,PyTorch就彻底支持它,并提供了一组丰富的特性来管理试用版。还有一些更先进的基于云的解决方案,好比Weights&Biases、[Alchemy](https://github.com/catalyst team/alchemy)和TensorBoard.dev,这些解决方案使得在多台机器上监控和比较训练变得更容易。
当使用Tensorboard时,我一般记录这样一组指标:
-
学习率和其余可能改变的优化参数(动量,重量衰减,等等) -
用于数据预处理和模型内部的时间 -
贯穿训练和验证的损失(每一个batch和每一个epoch的平均值) -
跨训练和验证的度量 -
训练session的超参数最终值 -
混淆矩阵,Precision-Recall曲线,AUC(若是适用) -
模型预测的可视化(如适用)
一图胜千言
直观地观察模型的预测是很是重要的。有时训练数据是有噪声的;有时,模型会过拟合图像的伪影。经过可视化最好的和最差的batch(基于损失或你感兴趣的度量),你能够对模型执行良好和糟糕的状况进行有价值的洞察。
建议5 — 可视化每一个epoch中最好和最坏的batch。它可能会给你宝贵的看法。
Catalyst用户提示:这里是使用可视化回调的示例:https://github.com/BloodAxe/Catalyst-Inria-Segmentation-Example/blob/master/fit_predict.py#L258
例如,在全球小麦检测挑战中,咱们须要在图像上检测小麦头。经过可视化最佳batch的图片(基于mAP度量),咱们看到模型在寻找小物体方面作得近乎完美。
相反,当咱们查看最差一批的第一个样本时,咱们看到模型很难对大物体作出准确的预测。可视化分析为任何数据科学家都提供了宝贵的看法。
查看最差的batch也有助于发现数据标记中的错误。一般状况下,贴错标签的样本损失更大,所以会成为最差的batch。经过在每一个epoch对最糟糕的batch作一个视觉检查,你能够消除这些错误:
使用Dict
做为Dataset和Model的返回值
建议4 — 若是你的模型返回一个以上的值,使用
Dict
来返回结果,不要使用tuple
在复杂的模型中,返回多个输出并很多见。例如,目标检测模型一般返回边界框及其标签,在图像分割CNN-s中,咱们常常返回中间层的mask进行深度监督,多任务学习最近也很经常使用。
在许多开源实现中,我常常看到这样的东西:
# 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
...
对于做者来讲,我认为这是一种很是糟糕的从模型返回结果的方法。下面是我推荐的替代方法:
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 }
...
这个建议在某种程度上与“The Zen of Python”的设定产生了共鸣 —— “明确的比含蓄的更好”。遵循这一规则将使你的代码更清晰、更容易维护。
那么为何我认为第二种选择更好呢?有几个缘由:
-
返回值有一个显式的名称与它关联。你不须要记住元组中元素的确切顺序。 -
若是你须要访问返回的字典的一个特定元素,你能够经过它的名字来访问。 -
从模型中添加新的输出不会破坏代码。
使用Dict
,你甚至能够更改模型的行为,以按需返回额外的输出。例如,这里有一个简短的片断,演示了如何返回多个“主”输出和两个“辅助”输出来进行度量学习:
# 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玩具示例,能够将图像及其对应的标签做为元组返回。但当处理多任务或多输入模型,你想从数据集返回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
当你的代码中有Dictionaries时,你能够在任何地方使用名称常量引用输入/输出。遵循这条规则将使你的训练管道很是清晰和容易遵循:
# 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),
]
在训练中检测异常
建议5 — 在训练期间使用
torch.autograd.detect_anomaly()
查找算术异常
若是你在训练过程当中在损失/度量中看到NaNs或Inf,你的脑海中就会响起一个警报。它是你的管道中有问题的指示器。一般状况下,它可能由如下缘由引发:
-
模型或特定层的初始化很差(你能够经过观察梯度大小来检查哪些层) -
数学上不正确的运算(负数的 torch.sqrt()
,非正数的torch.log()
,等等) -
不当使用 torch.mean()
和torch.sum()
的reduction(zero-sized张量上的均值会获得nan,大张量上的sum容易致使溢出) -
在loss中使用 x.sigmoid()
(若是你须要在loss函数中使用几率,更好的方法是x.sigmoid().clamp(eps,1-eps
)以防止梯度消失) -
在Adam-like的优化器中的低epsilon值 -
在使用fp16的训练的时候没有使用动态损失缩放
为了找到你代码中第一次出现Nan/Inf的确切位置,PyTorch提供了一个简单易用的方法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
将其用于调试目的,不然就禁用它,异常检测会带来计算开销,并将训练速度下降10-15% 。
英文原文:https://towardsdatascience.com/efficient-pytorch-supercharging-training-pipeline-19a26265adae
请长按或扫描二维码关注本公众号
喜欢的话,请给我个好看吧!
本文分享自微信公众号 - AI公园(AI_Paradise)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。