图像分类
让模型效果更好
将模型应用到更广泛的数据类型
从猫狗识别到宠物分类
from fastai.vision.all import *
path = untar_data(URLs.PETS) # 使用PETS数据集
提供数据的常见方式:
- 每一个文件就代表一个数据
- 与数据相关的表格中,每一行都代表一个数据
数据集中有哪些内容:
Path.BASE_PATH = path
path.ls()
# (#2) [Path('annotations'),Path('images')]两个目录
(path/"images").ls()
# (#7393 [Path('images/Abyssinian_1.jpg'),Path('images/Abyssinian_10.jpg'),Path('images/Abyssinian_100.jpg'),Path('images/Abyssinian_100.mat'),Path('images/Abyssinian_101.jpg'),Path('images/Abyssinian_101.mat'),Path('images/Abyssinian_102.jpg'),Path('images/Abyssinian_102.mat'),Path('images/Abyssinian_103.jpg'),Path('images/Abyssinian_104.jpg')...]
#为前缀,集合中数据的项数,要提取文件名中宠物类别,不能单纯的以第一个下划线区分,因为有的名字较复杂。
fname = (path/"images").ls()[0]
使用正则表达式(regex)
,它指定了一个通用规则,用于确定另一个字符串是否能通过测试(即“匹配”正则表达式),也可以用于从另一个字符串中提取一个或多个特定部分。
此时,需要一个正则表达式从文件名中提取宠物品种。findall
函数。
re.findall(r'(.+)_\d+.jpg$', fname.name)
fastai中如果用正则表达式标注数据,可以使用RegexLabeller
类达到这一目的。
pets = DataBlock(blocks = (ImageBlock, CategoryBlock),
get_items=get_image_files,
splitter=RandomSplitter(seed=42),
get_y=using_attr(RegexLabeller(r'(.+)_\d+.jpg$'), 'name'),
item_tfms=Resize(460),
batch_tfms=aug_transforms(size=224, min_scale=0.75)) # 尺寸预处理的数据增强策略,最大限度地减少数据被损坏的情况,同时保持良好的性能
dls = pets.dataloaders(path/"images")
图像尺寸的预处理
数据清洗,数据增强等。
#id interpolations
#caption A comparison of fastai's data augmentation strategy (left) and the traditional approach (right).
dblock1 = DataBlock(blocks=(ImageBlock(), CategoryBlock()),
get_y=parent_label,
item_tfms=Resize(460))
# Place an image in the 'images/grizzly.jpg' subfolder where this notebook is located before running this
dls1 = dblock1.dataloaders([(Path.cwd()/'images'/'grizzly.jpg')]*100, bs=8)
dls1.train.get_idxs = lambda: Inf.ones
x,y = dls1.valid.one_batch()
_,axs = subplots(1, 2)
x1 = TensorImage(x.clone())
x1 = x1.affine_coord(sz=224)
x1 = x1.rotate(draw=30, p=1.)
x1 = x1.zoom(draw=1.2, p=1.)
x1 = x1.warp(draw_x=-0.2, draw_y=0.2, p=1.)
tfms = setup_aug_tfms([Rotate(draw=30, p=1, size=224), Zoom(draw=1.2, p=1., size=224),
Warp(draw_x=-0.2, draw_y=0.2, p=1., size=224)])
x = Pipeline(tfms)(x)
#x.affine_coord(coord_tfm=coord_tfm, sz=size, mode=mode, pad_mode=pad_mode)
TensorImage(x[0]).show(ctx=axs[0])
TensorImage(x1[0]).show(ctx=axs[1])
fastai的数据增强策略比传统的好。
检查和调试数据块
show_batch
方法
dls.show_batch(nrows=1, ncols=3)
summary
方法,这里没有使用Resize转换,产生了不同大小的图像
pets1 = DataBlock(blocks = (ImageBlock, CategoryBlock),
get_items=get_image_files,
splitter=RandomSplitter(seed=42),
get_y=using_attr(RegexLabeller(r'(.+)_\d+.jpg$'), 'name'))
pets1.summary(path/"images")
# 可以显示运行时有错误的地方。可以清楚的看到如何收集数据并将其拆分的,如何从一个文件名转换为一个样本的...以及无法在一个batch中处理形状不同的样本
Setting-up type transforms pipelines
Collecting items from C:\Users\19764\.fastai\data\oxford-iiit-pet\images
Found 7390 items
2 datasets of sizes 5912,1478
Setting up Pipeline: PILBase.create
Setting up Pipeline: partial -> Categorize -- {'vocab': None, 'sort': True, 'add_na': False}
Building one sample
Pipeline: PILBase.create
starting from
C:\Users\19764\.fastai\data\oxford-iiit-pet\images\saint_bernard_138.jpg
applying PILBase.create gives
PILImage mode=RGB size=500x375
Pipeline: partial -> Categorize -- {'vocab': None, 'sort': True, 'add_na': False}
starting from
C:\Users\19764\.fastai\data\oxford-iiit-pet\images\saint_bernard_138.jpg
applying partial gives
saint_bernard
applying Categorize -- {'vocab': None, 'sort': True, 'add_na': False} gives
TensorCategory(30)
Final sample: (PILImage mode=RGB size=500x375, TensorCategory(30))
Collecting items from C:\Users\19764\.fastai\data\oxford-iiit-pet\images
Found 7390 items
2 datasets of sizes 5912,1478
Setting up Pipeline: PILBase.create
Setting up Pipeline: partial -> Categorize -- {'vocab': None, 'sort': True, 'add_na': False}
Setting up after_item: Pipeline: ToTensor
Setting up before_batch: Pipeline:
Setting up after_batch: Pipeline: IntToFloatTensor -- {'div': 255.0, 'div_mask': 1}
Building one batch
Applying item_tfms to the first sample:
Pipeline: ToTensor
starting from
(PILImage mode=RGB size=500x375, TensorCategory(30))
applying ToTensor gives
(TensorImage of size 3x375x500, TensorCategory(30))
Adding the next 3 samples
No before_batch transform to apply
Collating items in a batch
Error! It's not possible to collate your items in a batch
Could not collate the 0-th members of your tuples because got the following shapes
torch.Size([3, 375, 500]),torch.Size([3, 199, 300]),torch.Size([3, 333, 500]),torch.Size([3, 430, 500])
开始训练
learn = vision_learner(dls, resnet34, metrics=error_rate)
learn.fine_tune(2)
DataBlock中添加,再训练
item_tfms=Resize(460),
batch_tfms=aug_transforms(size=224, min_scale=0.75))
损失是用来优化参数模型的指标。如果没有告诉fastai使用什么损失函数,fastai将根据数据类型和模型选择适当的损失函数,对于图像分类,fastai默认使用交叉熵损失。
交叉熵损失
优势:
- 即使因变量有两个以上的类别,它仍然有效
- 训练速度更快,更可靠
查看激活值和标签
x,y = dls.one_batch() # 从数据加载器中获得一批真实数据
preds,_ = learn.get_preds(dl=[(x,y)]) # 查看预测结果(神经网络最后一层的激活值)
# preds[0]
len(preds[0]),preds[0].sum()
get_preds
可以接收数据集索引,0代表训练集,1代表验证集或批迭代器作为输入。默认返回预测结果和对应的目标类别。
实际预测结果是0-1之间的37个概率,加起来总和是1。
交叉熵损失之softmax
为了将模型的激活值转换成符合上述的预测值,使用softmax的激活函数。
分类模型中,最后一层使用softmax激活函数来确保激活值在0-1之间,并且总和为1.softmax看起来像sigmoid函数。
plot_function(torch.sigmoid, min=-4,max=4)
torch.random.manual_seed(42)
acts = torch.randn((6,2))*2
acts.sigmoid()
(acts[:,0]-acts[:,1]).sigmoid()
比较两个神经网络激活值之间的差异
def softmax(x): return exp(x) / exp(x).sum(dim=1, keepdim=True)
softmax是sigmoid的多类别版本,只要有两个或两个以上的类别,并且这些类别的概率加起来必须是1,就用它。
sm_acts = torch.softmax(acts, dim=1)
直观地说,softmax 函数确实想从其他类中选择一个类,因此当我们知道每张图片都有明确的标签时,它是训练分类器的理想选择。(请注意,在推理过程中它可能不太理想,因为你可能希望你的模型有时告诉你它无法识别它在训练期间看到的一些类别,并且不因为它的激活值稍大就盲目选择它。在这种情况下,最好对每一列使用二元分类的方法和概念,并且都使用一个sigmoid激活函数来训练模型比较好。)
交叉熵损失之对数似然
用sigmoid的话,表示这10类互不相关,得到的10个概率值中的每个值代表输入这类的概率和不属于这类的概率,0-1之间的值。比如第4个值,代表输入第4类的值概率和不属于第4类的概率,和其它9个值没关系。经过sigmoid输出的10个值互不影响,只关注某一类的可能性概率是多大,每一类都是二分类,所以加起来也不等于1,可以是第一类得到的值0.9,第二个也是0.9。
用softmax就不一样了,它要综合考虑10个类,属于每个类的概率,这10个类相互影响,加起来是等于1的。
targ = tensor([0,1,0,1,1,0])
sm_acts
idx = range(6)
sm_acts[idx, targ]
from IPython.display import HTML
df = pd.DataFrame(sm_acts, columns=["3","7"])
df['targ'] = targ
df['idx'] = idx
df['result'] = sm_acts[range(6), targ]
t = df.style.hide_index()
#To have html code compatible with our script
html = t._repr_html_().split('</style>')[1]
html = re.sub(r'<table id="([^"]+)"\s*>', r'<table >', html)
display(HTML(html))
PyTorch提供和sm_acts
功能相同的函数nll_loss
。
-sm_acts[idx, targ]
# tensor([-0.6025, -0.4979, -0.1332, -0.0034, -0.4041, -0.3661])
F.nll_loss(sm_acts, targ, reduction='none')
# tensor([-0.6025, -0.4979, -0.1332, -0.0034, -0.4041, -0.3661])
使用对数函数
损失中使用的是概率,0-1,如0.99和0.999,尽管很接近,但是后者比前者能有高10倍的信心,所以把0和1之间的数转换成负无穷大和无穷大之间的数。自然而然地使用到对数(torch.log)。
plot_function(torch.log, min=0,max=1, ty='log(x)', tx='x')
Python中log使用自然数对数e作为底。
log(a*b) = log(a)+log(b)。当基础信号以指数或乘法递增时,对于对数是线性增加的。
nll_loss中的”nll“代表”负对数似然“,但它实际根本不取对数。它假设已经取得了对数。PyTorch中有一个名为log_softmax的函数,它以一种快速而准确的方式把对数和softmax结合起来,nll_loss设计为在log_softmax之后使用。
如果我们首先取softmax,然后取它的对数似然,这个组合叫交叉熵损失
。PyTorch中为nn.CrossEntropyLoss
(实际上,先做log_softmax,再做nll_loss)。
则之前
from IPython.display import HTML
df['loss'] = -torch.log(tensor(df['result']))
t = df.style.hide_index()
#To have html code compatible with our script
html = t._repr_html_().split('</style>')[1]
html = re.sub(r'<table id="([^"]+)"\s*>', r'<table >', html)
display(HTML(html))
观察一个数字的对数接近负无穷大,因为该数字接近于零。在本例中,由于结果与正确标签的预测概率相关,我们希望我们的损失函数在预测为“好”(接近 1)时返回一个小值,在预测为“坏”(接近 0)时返回一个大值。我们可以通过取对数的负数来实现这一点:
plot_function(lambda x: -1*torch.log(x), min=0,max=1, tx='x', ty='- log(x)', title = 'Log Loss when true label = 1')
计算交叉熵损失
loss_func = nn.CrossEntropyLoss()
loss_func(acts, targ)
# tensor(1.8045)
类形式(更倾向于使用这个)
F.cross_entropy(acts, targ)
F命名空间中的形式。
默认情况下,PyTorch损失函数取所有项目损失的平均值,可以更改设定:
nn.CrossEntropyLoss(reduction='none')(acts, targ)
# tensor([0.5067, 0.6973, 2.0160, 5.6958, 0.9062, 1.0048])
当我们考虑交叉熵损失的梯度时,它的一个有趣特征出现了。cross_entropy(a,b)的梯度只是softmax(a)-b。由于softmax(a)是模型的最终激活值,这意味着梯度与预测和目标之间的差异成正比。这与回归中的均方误差相同(假设没有最终激活函数)就直接与y_range相加,因为(a-b)**2
的梯度为2*(a-b)
。因为梯度是线性的,这意味着我们不会看到梯度的突然跳跃或指数级增长,这应该使得模型的训练过程更平滑。
模型解释
可以使用混淆矩阵(confusion_matrix)看模型的效果。
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix(figsize=(12,12), dpi=60)
可以使用most_confused
只显示混淆矩阵中预测不正确的单元格。
interp.most_confused(min_val=5) # 这里显示至少5个或更多错误类别
# [('staffordshire_bull_terrier', 'american_pit_bull_terrier', 6),
# ('Birman', 'Ragdoll', 5),
# ('Ragdoll', 'Birman', 5)]
[('Birman', 'Ragdoll', 7)]
改进模型
迁移学习,如何在不破坏预训练权重的情况下尽可能地微调预训练模型。
学习率查找器
设置太小,需要多个周期训练模型,并且会记住数据,导致过拟合。
learn = vision_learner(dls, resnet34, metrics=error_rate)
learn.fine_tune(1, base_lr=0.1) # 设大点试试
!不对劲啊🙃
学习率查找器(learning rate finder):从一个很小的学习率开始,计算出其损失,然后学习率每次倍增,直到损失更差。然后以下规则二选一
- 选择的损失比达到最小损失时少一个数量级(即最小值除以10)
- 选择曲线上损失明显减少的最后一点
learn = vision_learner(dls, resnet34, metrics=error_rate)
lr_min,lr_steep = learn.lr_find(suggest_funcs=(minimum, steep))
print(f"Minimum/10: {lr_min:.2e}, steepest point: {lr_steep:.2e}")
# Minimum/10: 1.00e-02, steepest point: 3.02e-03
上图可以看出,在1e-6到1e-3范围内,模型没有训练,之后损失减少,直到最小值,然后又增加。不希望学习率超过1e-1,因为这会导致训练出现发散的情况。
看上去似乎3e-3左右的学习率是合适的,试试看
learn = vision_learner(dls, resnet34, metrics=error_rate)
learn.fine_tune(2, base_lr=3e-3)
对数范围,在学习率查找器绘制的图像中,学习率通常是在对数尺度上表示的。这是因为在优化神经网络时,我们通常更关心学习率的数量级,而不是它的精确值。
例如,1e-3(或0.001)和1e-2(或0.01)之间的中点在线性尺度上是0.0055,但在对数尺度上,它会更接近于1e-3。这是因为在对数尺度上,每个单位的距离表示的是值的倍数,而不是值的增量。所以,对数尺度上的中点表示的是两个值的几何平均数,而不是它们的算术平均数。
在 fastai 中,你可以使用 Learner.lr_find
方法来找到一个好的学习率。这个方法会绘制一个损失与学习率的图像,其中学习率是在对数尺度上表示的。你可以通过查找损失最快下降的学习率来选择一个好的学习率。
算术平均数和几何平均数是两种不同的计算平均值的方法。
- 算术平均数是所有数值的和除以数值的数量。例如,数值 2, 4, 6 的算术平均数是 (2+4+6)/3 = 4。
- 几何平均数是所有数值的乘积的 n 次方根,其中 n 是数值的数量。例如,数值 2, 4, 8 的几何平均数是 (2*4*8)^(1/3) ≈ 4.16。
在对数尺度上,我们通常使用几何平均数,因为它可以给出数值的中间数量级。例如,对于数值 1e-3 和 1e-2,它们的几何平均数是 (1e-3 * 1e-2)^(1/2) = 3.16e-3,这个值在对数尺度上位于 1e-3 和 1e-2 之间。
解冻与迁移学习
卷积神经网络由多个线性层组成,每两层之间有一个非线性激活函数,然后是一个或多个最终的线性层,最后有一个激活函数,如softmax。最后一个线性层使用一个列数足够多的矩阵,以便输出的大小与模型中的类别数相同(假设在进行分类)。
当在迁移学习中进行微调时,最后的线性层用处不大,用新层替换,匹配新的任务。
新添加的层具有随机权重,在微调之前具有完全随机的输出。前几层网络可以寻找到通用的特征,如边缘和梯度,后几层网络仍能代表一些对我们有用的特征。
目标:训练的模型能记住预训练模型中具有普适且通用的一些特征,然后使用它们来解决特定任务,并仅根据特定任务的具体情况进行调整。
微调挑战:如何用能够正确实现期望任务的权重替换掉添加的线性层中的随机权重,而不破坏经过仔细训练的权重和其他层?
实现:告诉优化器只更新那些随机添加的最终层中的权重,不改变神经网络其余部分的权重。(形象说成是把那些预训练层冻结住)
fastai:当从预训练网络创建模型时,fastai会自动冻结所有预训练层。当调用微调方法时,fastai做两件事:
- 每一周期都只训练随机添加的层,冻结住所有的其他层
- 解冻所有层,并根据我们设定好的周期数对其进行训练
自定义fine_tune
训练过程,
learn.fine_tune?? # 查看fine_tune源代码
###
Signature:
learn.fine_tune(
epochs,
base_lr=0.002,
freeze_epochs=1,
lr_mult=100,
pct_start=0.3,
div=5.0,
*,
lr_max=None,
div_final=100000.0,
wd=None,
moms=None,
cbs=None,
reset_opt=False,
start_epoch=0,
)
Source:
@patch
@delegates(Learner.fit_one_cycle)
def fine_tune(self:Learner, epochs, base_lr=2e-3, freeze_epochs=1, lr_mult=100,
pct_start=0.3, div=5.0, **kwargs):
"Fine tune with `Learner.freeze` for `freeze_epochs`, then with `Learner.unfreeze` for `epochs`, using discriminative LR."
self.freeze()
self.fit_one_cycle(freeze_epochs, slice(base_lr), pct_start=0.99, **kwargs)
base_lr /= 2
self.unfreeze()
self.fit_one_cycle(epochs, slice(base_lr/lr_mult, base_lr), pct_start=pct_start, div=div, **kwargs)
File: f:\anconda3\envs\pytorch\lib\site-packages\fastai\callback\schedule.py
Type: method
fine_tune
是 fastai 库中的一个方法,用于对预训练模型进行微调。以下是参数的解释:
epochs
:整个数据集的训练次数。base_lr
:训练开始时的学习率。freeze_epochs
:在开始微调之前,冻结预训练层并训练新添加的层的周期数。lr_mult
:最后一层和第一层之间的学习率的比率。pct_start
:学习率调度的上升阶段所占的比例。div
:最大学习率和最小学习率之间的比率。lr_max
:学习率调度的最大学习率。div_final
:最终学习率和最小学习率之间的比率。wd
:权重衰减。moms
:动量。cbs
:要传递给训练器的回调函数。reset_opt
:是否在微调前重置优化器。start_epoch
:开始训练的周期。
这个方法首先冻结预训练模型的所有层,只训练新添加的层(比如在图像分类任务中的全连接层)。然后,它解冻所有的层,并使用学习率调度进行训练。学习率调度开始时,学习率会逐渐增加,然后再逐渐减小。这种策略可以帮助模型快速收敛,并避免陷入不良的局部最优。
直接调用底层方法更简单,使用fit_one_cycle
对随机添加的层进行几个周期的训练。以较低的学习率开始训练,然后在第一部分的训练中逐渐提高学习率,再在最后一部分的训练中逐渐降低学习率。
learn.fit_one_cycle??
Signature:
learn.fit_one_cycle(
n_epoch,
lr_max=None,
div=25.0,
div_final=100000.0,
pct_start=0.25,
wd=None,
moms=None,
cbs=None,
reset_opt=False,
start_epoch=0,
)
Source:
@patch
def fit_one_cycle(self:Learner, n_epoch, lr_max=None, div=25., div_final=1e5, pct_start=0.25, wd=None,
moms=None, cbs=None, reset_opt=False, start_epoch=0):
"Fit `self.model` for `n_epoch` using the 1cycle policy."
if self.opt is None: self.create_opt()
self.opt.set_hyper('lr', self.lr if lr_max is None else lr_max)
lr_max = np.array([h['lr'] for h in self.opt.hypers])
scheds = {'lr': combined_cos(pct_start, lr_max/div, lr_max, lr_max/div_final),
'mom': combined_cos(pct_start, *(self.moms if moms is None else moms))}
self.fit(n_epoch, cbs=ParamScheduler(scheds)+L(cbs), reset_opt=reset_opt, wd=wd, start_epoch=start_epoch)
File: f:\anconda3\envs\pytorch\lib\site-packages\fastai\callback\schedule.py
Type: method
fit_one_cycle
方法的参数解释如下:
n_epoch
:训练的周期数,即整个数据集的训练次数。lr_max
:学习率调度的最大学习率。div
:初始学习率与lr_max
的比率,用于计算初始学习率。div_final
:最终学习率与lr_max
的比率,用于计算最终学习率。pct_start
:在训练周期中,学习率从初始值增加到lr_max
的部分所占的比例。wd
:权重衰减系数,用于正则化模型参数。moms
:动量的最大值和最小值,用于调整优化器的动量。cbs
:需要添加到训练过程中的回调函数列表。reset_opt
:是否在训练开始时重置优化器。start_epoch
:训练开始的周期数,通常用于继续之前中断的训练。
learn = vision_learner(dls, resnet34, metrics=error_rate)
learn.fit_one_cycle(3, 3e-3)
然后解冻所有层:
learn.unfreeze()
再次运行lr_find
,因为要训练的层数更多,而权重已经训练了三个周期了,这意味着以前找到的学习率已经不再适合当前任务。
learn.lr_find()
# SuggestedLRs(valley=1.0964781722577754e-06)
该图与之前的完整训练有所不同,训练过程中没有出现损失的急剧下降,因为模型时训练过的。而在损失急剧上升之前,有一个稍微平坦的区域,在此取一个点,1e-6.
learn.fit_one_cycle(6, lr_max=1e-6)
预训练模型的最深层可能不需要像最后几层那样高的学习率,因此可能应该对不同的层使用不同的学习率,称为区别学习率。
区别学习率
因为已经经过预训练了,所以不希望预训练参数的学习率像之后添加的那么高,对模型进行微调的过程中,让后面的层比前面的层变化得更快时有意义的。
fastai默认使用区别学习率:对神经网络靠前的层使用较低的学习率,对靠后的层使用较高的学习率(尤其是添加了随机参数的层)。通过迁移学习,神经网络不同的层次应该以不同的速度被训练。
learn = vision_learner(dls, resnet34, metrics=error_rate)
learn.fit_one_cycle(3, 3e-3)
learn.unfreeze()
learn.fit_one_cycle(12, lr_max=slice(1e-6,1e-4))
如上,slice传递的第一个参数是神经网络(预训练层之后新加的层)最早一层的学习率,第二个值是最后一层的学习率,中间层的学习率将在该范围内成倍等间距地递增。
n_epoch
:训练的周期数,即整个数据集的训练次数。lr_max
:学习率调度的最大学习率。
slice(1e-6,1e-4)
表示学习率在训练过程中从 1e-6 变化到 1e-4。这是一种称为 “学习率切片” 的技术,它可以让不同的层使用不同的学习率,通常情况下,模型的前面几层的学习率会比后面几层的学习率小。这是因为前面的层通常负责提取更通用的特征,而后面的层负责提取更具体的特征,所以我们通常希望在训练过程中更多地更新后面的层。
fastai可以通过一张图展现训练和验证过程中的损失变化情况:
learn.recorder.plot_loss()
训练集上损失变得越来越好,但最终验证集上损失的提升慢,甚至有些糟糕,损失变大的地方,这是模型开始过拟合的预警点。但不影响指标的提高。
选择训练的周期数
如何选择训练神经网络的周期数(epochs):
- 训练周期数的选择往往受到时间限制,而不是模型的泛化能力或准确率。因此,首先应选择一个在你愿意等待的时间内可以完成的训练周期数。
- 通过观察训练和验证损失图,以及你选择的度量指标,如果你发现在最后的周期中,这些指标仍在改善,那么说明你的训练周期数并未过多。
- 另一方面,你可能会发现你选择的度量指标在训练结束时变得更糟。这可能是因为模型过于自信,或者错误地记忆了数据。我们实际关心的是后者。损失函数只是我们用来让优化器可以进行微分和优化的工具,而不是我们实际关心的东西。
- 在 1cycle 训练出现之前,通常会在每个周期结束时保存模型,然后从所有周期中选择准确率最高的模型。这被称为 “早停“(early stopping)。但这种方法不太可能给出最好的结果,因为中间的周期发生在学习率还未达到可以找到最佳结果的小值之前。因此,如果你发现你的模型过拟合了,你应该从头开始重新训练你的模型,并根据你之前找到的最佳结果来选择总的训练周期数。
- 如果你有更多的时间来训练更多的周期,你可能会希望使用这些时间来训练更多的参数,即使用更深的架构。
fastai中如何使用早停(early stopping)来防止过拟合?
在 fastai 中,你可以使用 EarlyStoppingCallback
来实现早停。这个回调函数会在训练过程中监控一个指定的度量(如验证损失),并在该度量连续几个周期不再改善时停止训练,以防止模型过拟合。
以下是如何在 fastai 中使用 EarlyStoppingCallback
的示例:
from fastai.callback import EarlyStoppingCallback
learn = cnn_learner(dls, resnet34, metrics=accuracy)
learn.fit_one_cycle(10, cbs=EarlyStoppingCallback(monitor='valid_loss', min_delta=0.01, patience=3))
在这个示例中,EarlyStoppingCallback
会监控验证损失 (monitor='valid_loss'
)。如果验证损失在连续 3 个周期 (patience=3
) 内的改善都小于 0.01 (min_delta=0.01
),那么训练将会被停止。
注意,EarlyStoppingCallback
只能在 fit
或 fit_one_cycle
等训练方法中使用,不能在 Learner
的构造函数中使用。
更深的网络架构
神经网络模型的大小(参数数量)对模型性能的影响,以及如何处理深度模型训练中可能遇到的问题。
- 一般来说,参数更多的模型能更准确地拟合数据,但也更容易过拟合,因为有更多的参数可以用来记住训练数据的特定细节。
- 由于我们通常使用预训练的模型,所以我们需要选择已经被预训练过的层数。这就是为什么实践中的神经网络架构通常只有少数几种变体,例如 ResNet 就有 18、34、50、101 和 152 层的变体。
- 使用更深的模型需要更多的 GPU 内存,可能会导致内存溢出错误。解决这个问题的方法是减小批次大小,即一次通过模型的图像数量。
- 更深的架构需要更长的训练时间。一种可以显著加速训练的技术是混合准确率训练,即在可能的情况下使用准确率较低的数字(半准确率浮点,也称fp16)进行训练。张量核这一特殊功能,用更少的内存提高训练速度,在 fastai 中,可以通过在创建
Learner
后添加to_fp16()
来启用这个功能。 - 你无法提前知道哪种架构最适合你的问题,需要通过实际训练来尝试。
from fastai.callback.fp16 import *
learn = vision_learner(dls, resnet50, metrics=error_rate).to_fp16()
learn.fine_tune(6, freeze_epochs=3)
更大的模型不一定是更好的模型,在开始扩大模型之前,可以先尝试小模型。
https://nbviewer.org/github/fastai/fastbook/blob/master/05_pet_breeds.ipynb
多标签分类与回归
迁移学习要注意更改损失函数,
用于单标签分类的nn.CrossEntrophyLoss
用于多标签分类的nn.BCEWithLogitsLoss
用于回归的nn.MSELoss