大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
个人主页-Sonhhxg_柒的博客_CSDN博客
欢迎各位→点赞 + 收藏⭐️ + 留言
系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟
文章目录
建立基线
通用优化器
Momentum
RMSProp
Adam
Decoupled Weight Decay(解耦权重衰减)
Callbacks(回调)
创建回调
回调排序和异常
结论
您现在知道如何为计算机视觉、自然语言处理、表格分析和协同过滤创建最先进的架构,并且知道如何快速训练它们。所以我们完成了,对吧?还不完全是。我们仍然需要探索更多的训练过程。
我们在第 4 章解释了随机梯度下降的基础:将一个 mini-batch 传递给模型,将其与我们的 target 与损失函数,然后在使用公式更新权重之前计算此损失函数关于每个权重的梯度:
new_weight = weight - lr * weight.grad
我们在训练循环中从头开始实现这个,并看到 PyTorch 提供了一个简单的nn.SGD
类来为 我们的每个参数。在本章中,我们将使用灵活的基础构建一些更快的优化器。但这并不是我们想要在训练过程中改变的全部。对于训练循环的任何调整,我们都需要一种方法来将一些代码添加到 SGD 的基础上。fastai 图书馆有一个回调系统来做这件事,我们会教你所有这一切。
让我们从标准 SGD 开始以获得基线;然后我们将介绍最常用的优化器。
首先,我们将使用普通 SGD 创建一个基线,并将其与 fastai 的默认优化器进行比较。我们将从get_data
使用我们在 第 14 章中使用 的方法获取 Imagenette :
dls = get_data(URLs.IMAGENETTE_160, 160, 128)
我们将创建一个没有预训练的 ResNet-34 并传递收到的任何参数:d
def get_learner(**kwargs):
return cnn_learner(dls, resnet34, pretrained=False,
metrics=accuracy, **kwargs).to_fp16()
这是默认的 fastai 优化器,具有通常的 3e-3 学习率:
learn = get_learner()
learn.fit_one_cycle(3, 0.003)
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 2.571932 | 2.685040 | 0.322548 | 00:11 |
1 | 1.904674 | 1.852589 | 0.437452 | 00:11 |
2 | 1.586909 | 1.374908 | 0.594904 | 00:11 |
现在让我们试试普通的 SGD。我们可以通过opt_func
(优化函数)来cnn_learner
让 fastai 使用任何优化器:
learn = get_learner(opt_func=SGD)
首先要看的是lr_find
:
learn.lr_find()
(0.017378008365631102, 3.019951861915615e-07)
看起来我们需要使用比通常使用的更高的学习率:
learn.fit_one_cycle(3, 0.03, moms=(0,0,0))
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 2.969412 | 2.214596 | 0.242038 | 00:09 |
1 | 2.442730 | 1.845950 | 0.362548 | 00:09 |
2 | 2.157159 | 1.741143 | 0.408917 | 00:09 |
因为用动量加速 SGD 是个好主意,fastai 默认在fit_one_cycle
中这样做,所以我们用 关闭它 moms=(0,0,0)
。我们很快就会讨论势头。
显然,纯 SGD 的训练速度不如我们希望的那样快。因此,让我们学习一些技巧来加速训练吧!
要构建我们的加速 SGD 技巧,我们需要从一个很好的灵活优化器基础开始。在 fastai 之前没有图书馆提供这样的基础,但在 fastai 期间 在开发过程中,我们意识到我们在学术文献中看到的所有优化器改进都可以使用优化器回调来处理。这些是我们可以在优化器中组合、混合和匹配以构建优化器步骤的小代码片段。它们被 fastai 的轻量级Optimizer
类调用。这些是Optimizer
我们在本书中使用的两个关键方法的定义:
def zero_grad(self):
for p,*_ in self.all_params():
p.grad.detach_()
p.grad.zero_()
def step(self):
for p,pg,state,hyper in self.all_params():
for cb in self.cbs:
state = _update(state, cb(p, **{**state, **hyper}))
self.state[p] = state
正如我们在从头开始训练 MNIST 模型时看到的那样,zero_grad
只需循环遍历模型的参数并将梯度设置为零。它还会调用detach_
,这会删除梯度计算的任何历史记录,因为在 之后将不再需要它zero_grad
。
更有趣的方法是step
,它循环遍历回调 ( cbs
) 并调用它们来更新参数(如果 有任何返回, 该_update
函数就会调用)。如您所见,它本身不执行任何 SGD 步骤。让我们看看如何将 SGD 添加到 .state.update
cb
Optimizer
Optimizer
这是一个执行单个 SGD 步骤的优化器回调,方法是乘以-lr
梯度并将其添加到参数中(当Tensor.add_
在 PyTorch 中传递两个参数时,它们在相加之前相乘):
def sgd_cb(p, lr, **kwargs): p.data.add_(-lr, p.grad.data)
我们可以将其传递给Optimizer
使用cbs
参数;我们需要使用partial
,因为Learner
稍后将调用此函数来创建我们的优化器:
opt_func = partial(Optimizer, cbs=[sgd_cb])
让我们看看这是否训练:
learn = get_learner(opt_func=opt_func)
learn.fit(3, 0.03)
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 2.730918 | 2.009971 | 0.332739 | 00:09 |
1 | 2.204893 | 1.747202 | 0.441529 | 00:09 |
2 | 1.875621 | 1.684515 | 0.445350 | 00:09 |
它的工作!这就是我们在 fastai 中从头开始创建 SGD 的方式。现在让我们看看这个“势头”是什么。
如第 4 章所述,SGD 可以被认为是站在山顶,并通过向最陡峭的斜坡方向迈出一步来逐步下降 在每个时间点。但是,如果我们有一个球滚下山怎么办?在每个给定点,它不会完全遵循梯度的方向,因为它会有动量。动量更大的球(例如,更重的球)会跳过小的颠簸和洞,更有可能到达崎岖不平的山脚下。另一方面,乒乓球会卡在每个小缝隙中。
那么我们如何才能将这个想法应用到 SGD 中呢?我们可以使用移动平均线,而不是仅使用当前梯度来进行我们的步骤:
weight.avg = beta * weight.avg + (1-beta) * weight.grad
new_weight = weight - lr * weight.avg
这beta
是我们选择的一些数字,它定义了使用多少动量。如果beta
是 0,第一个方程变为 weight.avg = weight.grad
,所以我们最终得到普通的 SGD。但如果它是一个接近 1 的数字,则选择的主要方向是前面步骤的平均值。(如果您做过一些统计,您可能会在第一个等式中识别出指数加权移动平均线,它通常用于对数据进行去噪并获得潜在趋势。)
请注意,我们写信是weight.avg
为了强调一个事实,即我们需要为模型的每个参数存储移动平均值(它们都有自己的独立移动平均值)。
图 16-1显示了单个参数的噪声数据示例,其中动量曲线绘制为红色,参数的梯度绘制为蓝色。梯度先增加,然后减少,动量很好地遵循了总体趋势,而不会受到噪音的太大影响。
如果损失函数有我们需要导航的狭窄峡谷,它会特别有效:香草 SGD 会让我们从一侧弹跳到另一侧,而具有动量的 SGD 将使那些平滑地向下滚动。该参数beta
决定了我们使用的动量的强度:较小的beta
,我们更接近实际的梯度值,而较高的beta
,我们将主要朝着梯度平均值的方向前进,这将需要一段时间梯度的任何变化都会使趋势发生变化。
对于较大的beta
,我们可能会错过梯度已经改变方向并滚动到一个小的局部最小值。这是一个预期的副作用:直觉上,当我们向模型显示新输入时,它看起来像训练集中的某些东西,但不会完全一样。它将对应于损失函数中的一个点,该点接近我们在训练结束时得到的最小值,但不完全是那个最小值。因此,我们宁愿在最小范围内结束训练,附近的点具有大致相同的损失(或者,如果您愿意,损失尽可能平坦的点)。图 16-2显示了 图 16-1中的图表如何随着我们的变化而变化beta
。
我们可以在这些示例中看到,abeta
太高会导致梯度的整体变化被忽略。在带有动量的 SGD 中beta
,经常使用的值为 0.9。
fit_one_cycle
默认情况下从beta
0.95 开始,逐渐将其调整为 0.85,然后在训练结束时逐渐将其移回 0.95。让我们看看我们的训练如何将动量添加到普通 SGD。
为了给我们的优化器增加动力,我们首先需要跟踪移动平均梯度,我们可以用另一个回调来完成。当优化器回调返回 adict
时,它用于更新优化器的状态并在下一步传递回优化器。所以这个回调将在一个名为的参数中跟踪梯度平均值grad_avg
:
def average_grad(p, mom, grad_avg=None, **kwargs):
if grad_avg is None: grad_avg = torch.zeros_like(p.grad.data)
return {'grad_avg': grad_avg*mom + p.grad.data}
要使用它,我们只需要在我们的步骤函数中替换p.grad.data
为grad_avg
:
def momentum_step(p, lr, grad_avg, **kwargs): p.data.add_(-lr, grad_avg)
opt_func = partial(Optimizer, cbs=[average_grad,momentum_step], mom=0.9)
Learner
会自动安排mom
and lr
,所以fit_one_cycle
甚至会与我们的习惯一起工作Optimizer
:
learn = get_learner(opt_func=opt_func)
learn.fit_one_cycle(3, 0.03)
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 2.856000 | 2.493429 | 0.246115 | 00:10 |
1 | 2.504205 | 2.463813 | 0.348280 | 00:10 |
2 | 2.187387 | 1.755670 | 0.418853 | 00:10 |
learn.recorder.plot_sched()
我们仍然没有取得很好的结果,所以让我们看看我们还能做些什么。
RMSProp 是 SGD 的另一个变体,由 Geoffrey Hinton 在 他的 Coursera 课程“机器学习的神经网络”的第 6e 讲中介绍。与 SGD 的主要区别在于它使用 自适应学习率:不是对每个参数使用相同的学习率,每个参数都有自己的特定学习率,由全局学习率控制。这样,我们可以通过为需要大量改变的权重提供更高的学习率来加快训练速度,而那些足够好的权重则获得较低的学习率。
我们如何决定哪些参数应该有高学习率,哪些不应该?我们可以查看梯度以获得一个想法。如果一个参数的梯度已经接近零一段时间,那么该参数将需要更高的学习率,因为损失是平坦的。另一方面,如果梯度到处都是,我们可能应该小心并选择较低的学习率以避免发散。我们不能只对梯度进行平均以查看它们是否变化很大,因为一个大的正数和一个大的负数的平均值接近于零。相反,我们可以使用通常的技巧,即取绝对值或平方值(然后取均值后的平方根)。
再一次,为了确定噪声背后的一般趋势,我们将使用移动平均值——具体来说,是梯度平方的移动平均值。然后我们将使用当前梯度(方向)除以这个移动平均值的平方根来更新相应的权重(这样,如果它低,有效学习率会更高,如果它高,则有效学习率会更低):
w.square_avg = alpha * w.square_avg + (1-alpha) * (w.grad ** 2)
new_w = w - lr * w.grad / math.sqrt(w.square_avg + eps)
添加( epsiloneps
) 是为了数值稳定性(通常设置为 1e-8),默认值通常为 0.99。alpha
我们可以Optimizer
通过做与为 所做的大致相同的事情 来添加它avg_grad
,但有一个额外的**2
:
def average_sqr_grad(p, sqr_mom, sqr_avg=None, **kwargs):
if sqr_avg is None: sqr_avg = torch.zeros_like(p.grad.data)
return {'sqr_avg': sqr_mom*sqr_avg + (1-sqr_mom)*p.grad.data**2}
我们可以像以前一样定义阶跃函数和优化器:
def rms_prop_step(p, lr, sqr_avg, eps, grad_avg=None, **kwargs):
denom = sqr_avg.sqrt().add_(eps)
p.data.addcdiv_(-lr, p.grad, denom)
opt_func = partial(Optimizer, cbs=[average_sqr_grad,rms_prop_step],
sqr_mom=0.99, eps=1e-7)
让我们试试看:
learn = get_learner(opt_func=opt_func)
learn.fit_one_cycle(3, 0.003)
epoch | train_loss | vaild_loss | accuracy | time |
---|---|---|---|---|
0 | 2.766912 | 1.845900 | 0.402548 | 00:11 |
1 | 2.194586 | 1.510269 | 0.504459 | 00:11 |
2 | 1.869099 | 1.447939 | 0.544968 | 00:11 |
好多了!现在我们只需要将这些想法结合在一起,我们就有了 fastai 的默认优化器 Adam。
Adam 将 SGD 的思想与动量和 RMSProp 混合在一起:它使用梯度的移动平均值作为方向并除以 梯度平方的移动平均值的平方根,为每个参数提供自适应学习率。
Adam 计算移动平均线的方式还有一个不同之处。它采用无偏移动平均线,即
w.avg = beta * w.avg + (1-beta) * w.grad
unbias_avg = w.avg / (1 - (beta**(i+1)))
如果我们是第i
-th 次迭代(像 Python 一样从 0 开始)。这个除数 1 - (beta**(i+1))
确保无偏平均值看起来更像开始时的梯度(因为beta < 1
,分母很快接近 1)。
将所有内容放在一起,我们的更新步骤如下所示:
w.avg = beta1 * w.avg + (1-beta1) * w.grad
unbias_avg = w.avg / (1 - (beta1**(i+1)))
w.sqr_avg = beta2 * w.sqr_avg + (1-beta2) * (w.grad ** 2)
new_w = w - lr * unbias_avg / sqrt(w.sqr_avg + eps)
至于RMSProp,eps
一般设置为1e-8, (beta1,beta2)
文献建议默认为(0.9,0.999)
.
在 fastai 中,Adam 是我们使用的默认优化器,因为它允许更快的训练,但我们发现它beta2=0.99
更适合我们正在使用的调度类型。beta1
是动量参数,我们moms
在调用 时用参数指定fit_one_cycle
。至于 eps
,fastai 使用默认值 1e-5。eps
不仅对数值稳定性有用。更高eps
的限制了调整后的学习率的最大值。举个极端的例子,如果eps
是1,那么adjusted learning永远不会高于base learning rate。
我们不会在书中显示所有代码,而是让您查看 fastai 的O'Reilly Media - Technology and Business Training 24_O[GitHub 存储库] 中的优化器笔记本(浏览 _nbs文件夹并搜索名为optimizer的笔记本) . 您将看到我们目前展示的所有代码,以及 Adam 和其他优化器,以及大量示例和测试。
当我们从 SGD 转到 Adam 时发生的一件事是我们应用权重衰减的方式,它可能会产生重要的后果。
我们在第 8 章讨论过的权重衰减等同于(在普通 SGD 的情况下)用以下内容更新参数:
new_weight = weight - lr*weight.grad - lr*wd*weight
该公式的最后一部分解释了该技术的名称:每个权重衰减一个因子lr * wd
。
权重衰减的另一个名称是L2 正则化,它包括将所有权重平方和添加到损失(乘以权重衰减)。正如我们在第 8 章中看到的,这可以直接用梯度表示:
weight.grad += wd*weight
对于 SGD,这两个公式是等价的。然而,这种等价性仅适用于标准 SGD,因为正如我们在动量、RMSProp 或 Adam 中看到的那样,更新有一些围绕梯度的附加公式。
大多数库使用第二种公式,但 Ilya Loshchilov 和 Frank Hutter 在 “Decoupled Weight Decay Regularization”中指出,第一种是 Adam 优化器或动量的唯一正确方法,这就是 fastai 将其设为默认值的原因。
现在你知道隐藏在行后面的一切了 learn.fit_one_cycle
!
然而,优化器只是训练过程的一部分。当你需要用fastai改变训练循环时,你不能直接改变库里面的代码。相反,我们设计了一个回调系统,让您可以在独立的块中编写您喜欢的任何调整,然后您可以混合和匹配。
有时你需要稍微改变一下事情的运作方式。事实上,我们已经看到了这样的例子:Mixup、fp16 training、resetting the 每个时期之后的模型用于训练 RNN,等等。我们如何着手对培训过程进行这些调整?
我们已经看到了基本的训练循环,在Optimizer
类的帮助下,对于一个 epoch 看起来像这样:
for xb,yb in dl:
loss = loss_func(model(xb), yb)
loss.backward()
opt.step()
opt.zero_grad()
图 16-3显示了如何描绘它。
深度学习从业者定制训练循环的常用方法是复制现有的训练循环,然后将进行特定更改所需的代码插入其中。这就是您在网上找到的几乎所有代码的外观。但它有严重的问题。
一些特定的调整训练循环不太可能满足您的特定需求。可以对训练循环进行数百次更改,这意味着有数十亿种可能的排列。您不能只从这里的训练循环中复制一个调整,从那里的训练循环中复制另一个调整,并期望它们一起工作。每个都将基于对其工作环境的不同假设,使用不同的命名约定,并期望数据采用不同的格式。
我们需要一种方法允许用户在训练循环的任何部分插入他们自己的代码,但要以一致且定义明确的方式进行。计算机科学家已经想出了一个优雅的解决方案:回调。回调是您编写并在预定义点注入另一段代码的一段代码。事实上,回调已经与深度学习训练循环一起使用多年。问题在于,在以前的库中,只能在一小部分可能需要的地方注入代码——更重要的是,回调无法完成它们需要做的所有事情。
为了像手动复制和粘贴训练循环并直接向其中插入代码一样灵活,回调必须能够读取训练循环中可用的每条可能信息,根据需要修改所有信息,并完全控制何时应该终止批次、时期甚至整个训练循环。fastai 是第一个提供所有这些功能的库。它修改了训练循环,使其看起来如图 16-4 所示。
这种方法的有效性在过去几年中得到了证实——通过使用 fastai 回调系统,我们能够实现我们尝试的每一篇新论文,并满足每一个用户修改训练循环的请求。训练循环本身不需要修改。 图 16-5仅显示了一些已添加的回调。
这很重要,因为这意味着无论我们头脑中有什么想法,我们都可以实施。我们永远不需要深入研究 PyTorch 或 fastai 的源代码,然后拼凑一个一次性系统来试验我们的想法。当我们实施自己的回调来发展我们自己的想法时,我们知道它们将与 fastai 提供的所有其他功能一起工作——因此我们将获得进度条、混合精度训练、超参数退火等。
另一个优点是它可以轻松地逐渐删除或添加功能并执行消融研究。您只需要调整传递给适合函数的回调列表。
例如,这是为每批训练循环运行的 fastai 源代码:
try:
self._split(b); self('begin_batch')
self.pred = self.model(*self.xb); self('after_pred')
self.loss = self.loss_func(self.pred, *self.yb); self('after_loss')
if not self.training: return
self.loss.backward(); self('after_backward')
self.opt.step(); self('after_step')
self.opt.zero_grad()
except CancelBatchException: self('after_cancel_batch')
finally:
表单self('...')
的调用是调用回调的地方。如您所见,这发生在每一步之后。回调将接收整个训练状态,也可以对其进行修改。例如,输入数据和目标标签分别为 inself.xb
和self.yb
;回调可以修改这些以修改训练循环看到的数据。它还可以修改self.loss
甚至渐变。
让我们通过编写回调看看它在实践中是如何工作的。
当您想编写自己的回调时,可用事件的完整列表 如下:
begin_fit
在做任何事情之前被调用;初始设置的理想选择。
begin_epoch
在每个纪元开始时调用;对于您需要在每个时期重置的任何行为很有用。
begin_train
在纪元的训练部分开始时调用。
begin_batch
在每个批次开始时调用,就在绘制该批次之后。它可用于执行批处理所需的任何设置(如超参数调度)或在输入/目标进入模型之前更改输入/目标(例如,通过应用 Mixup)。
after_pred
在计算批处理模型的输出后调用。它可用于在输入损失函数之前更改该输出。
after_loss
在计算完损失后,但在向后传递之前调用。它可用于对损失添加惩罚(例如,RNN 训练中的 AR 或 TAR)。
after_backward
在向后传递之后但在参数更新之前调用。它可用于在所述更新之前对渐变进行更改(例如,通过渐变裁剪)。
after_step
在步骤之后和梯度归零之前调用。
after_batch
在批处理结束时调用,以在下一个批处理之前执行任何所需的清理。
after_train
在一个纪元的训练阶段结束时调用。
begin_validate
在 epoch 的验证阶段开始时调用;对于专门用于验证的任何设置很有用。
after_validate
在纪元的验证部分结束时调用。
after_epoch
在纪元结束时调用,用于在下一个纪元之前进行任何清理。
after_fit
在训练结束时调用,进行最后的清理。
此列表的元素可用作特殊变量的属性event
,因此您只需event.
在笔记本中键入并按 Tab 键即可查看所有选项的列表
让我们看一个例子。您还记得 第 12 章中我们如何需要确保reset
在每个时期的训练和验证开始时调用我们的特殊方法吗?我们使用ModelResetter
fastai 提供的回调来为我们做这件事。但它究竟是如何工作的呢?这是该类的完整源代码:
class ModelResetter(Callback):
def begin_train(self): self.model.reset()
def begin_validate(self): self.model.reset()
是的,实际上就是这样!它只是做了我们在前一段中所说的:在完成一个 epoch 的训练或验证后,调用一个名为reset
.
回调通常像这个一样“简短而甜蜜”。事实上,让我们再看一个。这是添加 RNN 正则化(AR 和 TAR)的回调的 fastai 源代码:
class RNNRegularizer(Callback):
def __init__(self, alpha=0., beta=0.): self.alpha,self.beta = alpha,beta
def after_pred(self):
self.raw_out,self.out = self.pred[1],self.pred[2]
self.learn.pred = self.pred[0]
def after_loss(self):
if not self.training: return
if self.alpha != 0.:
self.learn.loss += self.alpha * self.out[-1].float().pow(2).mean()
if self.beta != 0.:
h = self.raw_out[-1]
if len(h)>1:
self.learn.loss += self.beta * (h[:,1:] - h[:,:-1]
).float().pow(2).mean()
自己编码
回去重读“Activation Regularization and Temporal Activation Regularization”,然后再看看这里的代码。确保您了解它在做什么以及为什么。
在这两个示例中,请注意我们如何通过直接检查self.model
或来访问训练循环的属性self.pred
。那是因为 a总是会尝试获取它在与之关联的Callback
内部没有的属性。Learner
这些是self.learn.model
或的快捷方式self.learn.pred
。请注意,它们适用于读取属性,但不适用于写入属性,这就是为什么当RNNRegularizer
更改损失或预测时,您会看到self.learn.loss =
或self.learn.pred =
。
在编写回调时,可以使用以下属性Learner
:
model
用于训练/验证的模型。
data
底层的DataLoaders
。
loss_func
使用的损失函数。
opt
用于更新模型参数的优化器。
opt_func
用于创建优化器的函数。
cbs
包含所有Callback
s 的列表。
dl
当前DataLoader
用于迭代。
x
/xb
从中提取的最后一个输入self.dl
(可能由回调修改)。xb
始终是一个元组(可能只有一个元素),并且x
是去元组的。您只能分配给xb
.
y
/yb
从中提取的最后一个目标self.dl
(可能由回调修改)。yb
始终是一个元组(可能只有一个元素),并且y
是去元组的。您只能分配给yb
.
pred
最后的预测来自self.model
(可能被回调修改)。
loss
最后计算的损失(可能被回调修改)。
n_epoch
本次训练的轮数。
n_iter
当前 中的迭代次数self.dl
。
epoch
当前纪元索引(从 0 到n_epoch-1
)。
iter
当前迭代索引在self.dl
(从 0 到n_iter-1
)。
以下属性由添加TrainEvalCallback
并且应该可用,除非您不厌其烦地删除该回调:
train_iter
自本次培训开始以来完成的培训迭代次数
pct_train
训练迭代完成的百分比(从 0 到 1)
training
指示我们是否处于训练模式的标志
以下属性由添加Recorder
并且应该可用,除非您特意删除该回调:
smooth_loss
训练损失的指数平均版本
回调还可以通过使用 异常系统来中断训练循环的任何部分。
有时回调需要能够告诉 fastai 跳过一个批次或一个时期,或者完全停止训练。例如,考虑 TerminateOnNaNCallback
. 这个方便的回调将在损失变为无限或NaN
(不是数字)时自动停止训练。这是此回调的 fastai 源代码:
class TerminateOnNaNCallback(Callback):
run_before=Recorder
def after_batch(self):
if torch.isinf(self.loss) or torch.isnan(self.loss):
raise CancelFitException
该行raise CancelFitException
告诉训练循环在此时中断训练。训练循环捕获此异常并且不运行任何进一步的训练或验证。可用的回调控制流异常如下:
CancelBatchException
跳过这批的其余部分并转到after_batch
。
CancelTrainException
跳过 epoch 的其余训练部分并转到after_train
。
CancelValidException
跳过 epoch 的其余验证部分并转到after_validate
.
CancelEpochException
跳过这个纪元的其余部分并转到after_epoch
。
CancelFitException
中断训练并转到after_fit
。
您可以检测是否发生了这些异常之一,并添加在发生以下事件后立即执行的代码:
after_cancel_batch
CancelBatchException
在继续之前立即到达after_batch
after_cancel_train
CancelTrainException
在继续之前立即到达after_train
after_cancel_valid
CancelValidException
在继续之前立即到达after_valid
after_cancel_epoch
CancelEpochException
在继续之前立即到达after_epoch
after_cancel_fit
CancelFitException
在继续之前立即到达after_fit
有时需要以特定顺序调用回调。例如,在 的情况下TerminateOnNaNCallback
,重要的是 在此回调之后Recorder
运行它after_batch
,以避免注册NaN
损失。您可以在回调中指定run_before
(此回调必须在...之前运行)或run_after
(此回调必须在...之后运行)以确保您需要的顺序。
在本章中,我们仔细研究了训练循环,探索了 SGD 的变体以及为什么它们可以更强大。在撰写本文时,开发新的优化器是一个活跃的研究领域,因此当您阅读本章时,本书网站上可能已经有一个附录介绍了新的变体。请务必查看我们的通用优化器框架如何帮助您快速实施新的优化器。
我们还检查了强大的回调系统,它允许您通过检查和修改每个步骤之间您喜欢的任何参数来自定义训练循环的每一部分。