FastAI 之书(面向程序员的 FastAI)(二)(3)https://developer.aliyun.com/article/1482983
现在让我们看看一个权重的微小变化对准确性的影响是什么:
weights[0] *= 1.0001
preds = linear1(train_x) ((preds>0.0).float() == train_y).float().mean().item()
0.4912068545818329
正如我们所看到的,我们需要梯度来通过 SGD 改进我们的模型,为了计算梯度,我们需要一个损失函数,它代表了我们的模型有多好。这是因为梯度是损失函数如何随着对权重的微小调整而变化的度量。
因此,我们需要选择一个损失函数。显而易见的方法是使用准确性作为我们的度量标准,也作为我们的损失函数。在这种情况下,我们将为每个图像计算我们的预测,收集这些值以计算总体准确性,然后计算每个权重相对于总体准确性的梯度。
不幸的是,我们在这里有一个重要的技术问题。函数的梯度是其斜率,或者是其陡峭程度,可以定义为上升与下降——也就是说,函数值上升或下降的幅度,除以我们改变输入的幅度。我们可以用数学方式写成:
(y_new – y_old) / (x_new – x_old)
当x_new
非常类似于x_old
时,这给出了梯度的良好近似,这意味着它们的差异非常小。但是,只有当预测从 3 变为 7,或者反之时,准确性才会发生变化。问题在于,从x_old
到x_new
的权重的微小变化不太可能导致任何预测发生变化,因此(y_new - y_old)
几乎总是为 0。换句话说,梯度几乎在任何地方都为 0。
权重值的微小变化通常不会改变准确性。这意味着使用准确性作为损失函数是没有用的——如果我们这样做,大多数时候我们的梯度将为 0,模型将无法从该数字中学习。
Sylvain 说
在数学术语中,准确性是一个几乎在任何地方都是常数的函数(除了阈值 0.5),因此它的导数几乎在任何地方都是零(在阈值处为无穷大)。这将导致梯度为 0 或无穷大,这对于更新模型是没有用的。
相反,我们需要一个损失函数,当我们的权重导致稍微更好的预测时,给出稍微更好的损失。那么,“稍微更好的预测”具体是什么样呢?在这种情况下,这意味着如果正确答案是 3,则分数稍高,或者如果正确答案是 7,则分数稍低。
现在让我们编写这样一个函数。它是什么形式?
损失函数接收的不是图像本身,而是模型的预测。因此,让我们做一个参数prds
,值在 0 和 1 之间,其中每个值是图像是 3 的预测。它是一个矢量(即,一个秩-1 张量),索引在图像上。
损失函数的目的是衡量预测值与真实值之间的差异,即目标(又称标签)。因此,让我们再做一个参数trgts
,其值为 0 或 1,告诉图像实际上是 3 还是不是 3。它也是一个矢量(即,另一个秩-1 张量),索引在图像上。
例如,假设我们有三幅图像,我们知道其中一幅是 3,一幅是 7,一幅是 3。假设我们的模型以高置信度(0.9
)预测第一幅是 3,以轻微置信度(0.4
)预测第二幅是 7,以公平置信度(0.2
),但是错误地预测最后一幅是 7。这意味着我们的损失函数将接收这些值作为其输入:
trgts = tensor([1,0,1]) prds = tensor([0.9, 0.4, 0.2])
这是一个测量predictions
和targets
之间距离的损失函数的第一次尝试:
def mnist_loss(predictions, targets): return torch.where(targets==1, 1-predictions, predictions).mean()
我们正在使用一个新函数,torch.where(a,b,c)
。这与运行列表推导[b[i] if a[i] else c[i] for i in range(len(a))]
相同,只是它在张量上运行,以 C/CUDA 速度运行。简单来说,这个函数将衡量每个预测离 1 有多远,如果应该是 1 的话,以及它离 0 有多远,如果应该是 0 的话,然后它将取所有这些距离的平均值。
阅读文档
学习 PyTorch 这样的函数很重要,因为在 Python 中循环张量的速度是 Python 速度,而不是 C/CUDA 速度!现在尝试运行help(torch.where)
来阅读此函数的文档,或者更好的是,在 PyTorch 文档站点上查找。
让我们在我们的prds
和trgts
上尝试一下:
torch.where(trgts==1, 1-prds, prds)
tensor([0.1000, 0.4000, 0.8000])
您可以看到,当预测更准确时,当准确预测更自信时(绝对值更高),以及当不准确预测更不自信时,此函数返回较低的数字。在 PyTorch 中,我们始终假设损失函数的较低值更好。由于我们需要一个标量作为最终损失,mnist_loss
取前一个张量的平均值:
mnist_loss(prds,trgts)
tensor(0.4333)
例如,如果我们将对一个“错误”目标的预测从0.2
更改为0.8
,损失将减少,表明这是一个更好的预测:
mnist_loss(tensor([0.9, 0.4, 0.8]),trgts)
tensor(0.2333)
mnist_loss
当前定义的一个问题是它假设预测总是在 0 和 1 之间。因此,我们需要确保这实际上是这种情况!恰好有一个函数可以做到这一点,让我们来看看。
Sigmoid
sigmoid
函数总是输出一个介于 0 和 1 之间的数字。它的定义如下:
def sigmoid(x): return 1/(1+torch.exp(-x))
PyTorch 为我们定义了一个加速版本,所以我们不需要自己的。这是深度学习中一个重要的函数,因为我们经常希望确保数值在 0 和 1 之间。它看起来是这样的:
plot_function(torch.sigmoid, title='Sigmoid', min=-4, max=4)
正如您所看到的,它接受任何输入值,正数或负数,并将其压缩为 0 和 1 之间的输出值。它还是一个只上升的平滑曲线,这使得 SGD 更容易找到有意义的梯度。
让我们更新mnist_loss
,首先对输入应用sigmoid
:
def mnist_loss(predictions, targets): predictions = predictions.sigmoid() return torch.where(targets==1, 1-predictions, predictions).mean()
现在我们可以确信我们的损失函数将起作用,即使预测不在 0 和 1 之间。唯一需要的是更高的预测对应更高的置信度。
定义了一个损失函数,现在是一个好时机回顾为什么这样做。毕竟,我们已经有了一个度量标准,即整体准确率。那么为什么我们定义了一个损失?
关键区别在于指标用于驱动人类理解,而损失用于驱动自动学习。为了驱动自动学习,损失必须是一个具有有意义导数的函数。它不能有大的平坦部分和大的跳跃,而必须是相当平滑的。这就是为什么我们设计了一个损失函数,可以对置信水平的小变化做出响应。这个要求意味着有时它实际上并不完全反映我们试图实现的目标,而是我们真正目标和一个可以使用其梯度进行优化的函数之间的妥协。损失函数是针对数据集中的每个项目计算的,然后在时代结束时,所有损失值都被平均,整体均值被报告为时代。
另一方面,指标是我们关心的数字。这些是在每个时代结束时打印的值,告诉我们我们的模型表现如何。重要的是,我们学会关注这些指标,而不是损失,来评估模型的性能。
SGD 和小批次
现在我们有了一个适合驱动 SGD 的损失函数,我们可以考虑学习过程的下一阶段涉及的一些细节,即根据梯度改变或更新权重。这被称为优化步骤。
要进行优化步骤,我们需要计算一个或多个数据项的损失。我们应该使用多少?我们可以为整个数据集计算并取平均值,或者可以为单个数据项计算。但这两种方法都不理想。为整个数据集计算将需要很长时间。为单个数据项计算将不会使用太多信息,因此会导致不精确和不稳定的梯度。您将费力更新权重,但只考虑这将如何改善模型在该单个数据项上的性能。
因此,我们做出妥协:我们一次计算几个数据项的平均损失。这被称为小批次。小批次中的数据项数量称为批次大小。较大的批次大小意味着您将从损失函数中获得更准确和稳定的数据集梯度估计,但这将需要更长时间,并且您将在每个时代处理较少的小批次。选择一个好的批次大小是您作为深度学习从业者需要做出的决定之一,以便快速准确地训练您的模型。我们将在本书中讨论如何做出这个选择。
使用小批次而不是在单个数据项上计算梯度的另一个很好的理由是,实际上,我们几乎总是在加速器上进行训练,例如 GPU。这些加速器只有在一次有很多工作要做时才能表现良好,因此如果我们可以给它们很多数据项来处理,这将是有帮助的。使用小批次是实现这一目标的最佳方法之一。但是,如果您一次给它们太多数据来处理,它们会耗尽内存——让 GPU 保持愉快也是棘手的!
正如您在第二章中关于数据增强的讨论中所看到的,如果我们在训练过程中可以改变一些东西,我们会获得更好的泛化能力。我们可以改变的一个简单而有效的事情是将哪些数据项放入每个小批次。我们通常不是简单地按顺序枚举我们的数据集,而是在每个时代之前随机洗牌,然后创建小批次。PyTorch 和 fastai 提供了一个类,可以为您执行洗牌和小批次整理,称为DataLoader
。
DataLoader
可以将任何 Python 集合转换为一个迭代器,用于生成多个批次,就像这样:
coll = range(15) dl = DataLoader(coll, batch_size=5, shuffle=True) list(dl)
[tensor([ 3, 12, 8, 10, 2]), tensor([ 9, 4, 7, 14, 5]), tensor([ 1, 13, 0, 6, 11])]
对于训练模型,我们不只是想要任何 Python 集合,而是一个包含独立和相关变量(模型的输入和目标)的集合。包含独立和相关变量元组的集合在 PyTorch 中被称为Dataset
。这是一个极其简单的Dataset
的示例:
ds = L(enumerate(string.ascii_lowercase)) ds
(#26) [(0, 'a'),(1, 'b'),(2, 'c'),(3, 'd'),(4, 'e'),(5, 'f'),(6, 'g'),(7, > 'h'),(8, 'i'),(9, 'j')...]
当我们将Dataset
传递给DataLoader
时,我们将得到许多批次,它们本身是表示独立和相关变量批次的张量元组:
dl = DataLoader(ds, batch_size=6, shuffle=True) list(dl)
[(tensor([17, 18, 10, 22, 8, 14]), ('r', 's', 'k', 'w', 'i', 'o')), (tensor([20, 15, 9, 13, 21, 12]), ('u', 'p', 'j', 'n', 'v', 'm')), (tensor([ 7, 25, 6, 5, 11, 23]), ('h', 'z', 'g', 'f', 'l', 'x')), (tensor([ 1, 3, 0, 24, 19, 16]), ('b', 'd', 'a', 'y', 't', 'q')), (tensor([2, 4]), ('c', 'e'))]
我们现在准备为使用 SGD 的模型编写我们的第一个训练循环!
把所有东西放在一起
是时候实现我们在图 4-1 中看到的过程了。在代码中,我们的过程将为每个时期实现类似于这样的东西:
for x,y in dl: pred = model(x) loss = loss_func(pred, y) loss.backward() parameters -= parameters.grad * lr
首先,让我们重新初始化我们的参数:
weights = init_params((28*28,1)) bias = init_params(1)
DataLoader
可以从Dataset
创建:
dl = DataLoader(dset, batch_size=256) xb,yb = first(dl) xb.shape,yb.shape
(torch.Size([256, 784]), torch.Size([256, 1]))
我们将对验证集执行相同的操作:
valid_dl = DataLoader(valid_dset, batch_size=256)
让我们创建一个大小为 4 的小批量进行测试:
batch = train_x[:4] batch.shape
torch.Size([4, 784])
preds = linear1(batch) preds
tensor([[-11.1002], [ 5.9263], [ 9.9627], [ -8.1484]], grad_fn=<AddBackward0>)
loss = mnist_loss(preds, train_y[:4])
tensor(0.5006, grad_fn=<MeanBackward0>)
现在我们可以计算梯度了:
loss.backward() weights.grad.shape,weights.grad.mean(),bias.grad
(torch.Size([784, 1]), tensor(-0.0001), tensor([-0.0008]))
让我们把所有这些放在一个函数中:
def calc_grad(xb, yb, model): preds = model(xb) loss = mnist_loss(preds, yb) loss.backward()
并测试它:
calc_grad(batch, train_y[:4], linear1) weights.grad.mean(),bias.grad
(tensor(-0.0002), tensor([-0.0015]))
但是看看如果我们调用两次会发生什么:
calc_grad(batch, train_y[:4], linear1) weights.grad.mean(),bias.grad
(tensor(-0.0003), tensor([-0.0023]))
梯度已经改变了!这是因为loss.backward
添加了loss
的梯度到当前存储的任何梯度中。因此,我们首先必须将当前梯度设置为 0:
weights.grad.zero_() bias.grad.zero_();
原地操作
PyTorch 中以下划线结尾的方法会原地修改它们的对象。例如,bias.zero_
会将张量bias
的所有元素设置为 0。
我们唯一剩下的步骤是根据梯度和学习率更新权重和偏差。当我们这样做时,我们必须告诉 PyTorch 不要对这一步骤进行梯度计算,否则当我们尝试在下一个批次计算导数时会变得混乱!如果我们将张量的data
属性赋值,PyTorch 将不会对该步骤进行梯度计算。这是我们用于一个时期的基本训练循环:
def train_epoch(model, lr, params): for xb,yb in dl: calc_grad(xb, yb, model) for p in params: p.data -= p.grad*lr p.grad.zero_()
我们还想通过查看验证集的准确性来检查我们的表现。要决定输出是否代表 3 或 7,我们只需检查它是否大于 0。因此,我们可以计算每个项目的准确性(使用广播,所以没有循环!)如下:
(preds>0.0).float() == train_y[:4]
tensor([[False], [ True], [ True], [False]])
这给了我们计算验证准确性的这个函数:
def batch_accuracy(xb, yb): preds = xb.sigmoid() correct = (preds>0.5) == yb return correct.float().mean()
我们可以检查它是否有效:
batch_accuracy(linear1(batch), train_y[:4])
tensor(0.5000)
然后把批次放在一起:
def validate_epoch(model): accs = [batch_accuracy(model(xb), yb) for xb,yb in valid_dl] return round(torch.stack(accs).mean().item(), 4)
validate_epoch(linear1)
0.5219
这是我们的起点。让我们训练一个时期,看看准确性是否提高:
lr = 1. params = weights,bias train_epoch(linear1, lr, params) validate_epoch(linear1)
0.6883
然后再做几次:
for i in range(20): train_epoch(linear1, lr, params) print(validate_epoch(linear1), end=' ')
0.8314 0.9017 0.9227 0.9349 0.9438 0.9501 0.9535 0.9564 0.9594 0.9618 0.9613 > 0.9638 0.9643 0.9652 0.9662 0.9677 0.9687 0.9691 0.9691 0.9696
看起来不错!我们的准确性已经接近“像素相似性”方法的准确性,我们已经创建了一个通用的基础可以构建。我们的下一步将是创建一个将处理 SGD 步骤的对象。在 PyTorch 中,它被称为优化器。
创建一个优化器
因为这是一个如此通用的基础,PyTorch 提供了一些有用的类来使实现更容易。我们可以做的第一件事是用 PyTorch 的nn.Linear
模块替换我们的linear
函数。模块是从 PyTorch nn.Module
类继承的类的对象。这个类的对象的行为与标准 Python 函数完全相同,您可以使用括号调用它们,它们将返回模型的激活。
nn.Linear
做的事情与我们的init_params
和linear
一样。它包含了权重和偏差在一个单独的类中。这是我们如何复制上一节中的模型:
linear_model = nn.Linear(28*28,1)
每个 PyTorch 模块都知道它有哪些可以训练的参数;它们可以通过parameters
方法获得:
w,b = linear_model.parameters() w.shape,b.shape
(torch.Size([1, 784]), torch.Size([1]))
我们可以使用这些信息创建一个优化器:
class BasicOptim: def __init__(self,params,lr): self.params,self.lr = list(params),lr def step(self, *args, **kwargs): for p in self.params: p.data -= p.grad.data * self.lr def zero_grad(self, *args, **kwargs): for p in self.params: p.grad = None
我们可以通过传入模型的参数来创建优化器:
opt = BasicOptim(linear_model.parameters(), lr)
我们的训练循环现在可以简化:
def train_epoch(model): for xb,yb in dl: calc_grad(xb, yb, model) opt.step() opt.zero_grad()
我们的验证函数不需要任何更改:
validate_epoch(linear_model)
0.4157
让我们把我们的小训练循环放在一个函数中,让事情变得更简单:
def train_model(model, epochs): for i in range(epochs): train_epoch(model) print(validate_epoch(model), end=' ')
结果与上一节相同:
train_model(linear_model, 20)
0.4932 0.8618 0.8203 0.9102 0.9331 0.9468 0.9555 0.9629 0.9658 0.9673 0.9687 > 0.9707 0.9726 0.9751 0.9761 0.9761 0.9775 0.978 0.9785 0.9785
fastai 提供了SGD
类,默认情况下与我们的BasicOptim
做相同的事情:
linear_model = nn.Linear(28*28,1) opt = SGD(linear_model.parameters(), lr) train_model(linear_model, 20)
0.4932 0.852 0.8335 0.9116 0.9326 0.9473 0.9555 0.9624 0.9648 0.9668 0.9692 > 0.9712 0.9731 0.9746 0.9761 0.9765 0.9775 0.978 0.9785 0.9785
fastai 还提供了Learner.fit
,我们可以使用它来代替train_model
。要创建一个Learner
,我们首先需要创建一个DataLoaders
,通过传入我们的训练和验证DataLoader
:
dls = DataLoaders(dl, valid_dl)
要创建一个Learner
而不使用应用程序(如cnn_learner
),我们需要传入本章中创建的所有元素:DataLoaders
,模型,优化函数(将传递参数),损失函数,以及可选的任何要打印的指标:
learn = Learner(dls, nn.Linear(28*28,1), opt_func=SGD, loss_func=mnist_loss, metrics=batch_accuracy)
现在我们可以调用fit
:
learn.fit(10, lr=lr)
epoch | train_loss | valid_loss | batch_accuracy | time |
0 | 0.636857 | 0.503549 | 0.495584 | 00:00 |
1 | 0.545725 | 0.170281 | 0.866045 | 00:00 |
2 | 0.199223 | 0.184893 | 0.831207 | 00:00 |
3 | 0.086580 | 0.107836 | 0.911187 | 00:00 |
4 | 0.045185 | 0.078481 | 0.932777 | 00:00 |
5 | 0.029108 | 0.062792 | 0.946516 | 00:00 |
6 | 0.022560 | 0.053017 | 0.955348 | 00:00 |
7 | 0.019687 | 0.046500 | 0.962218 | 00:00 |
8 | 0.018252 | 0.041929 | 0.965162 | 00:00 |
9 | 0.017402 | 0.038573 | 0.967615 | 00:00 |
正如您所看到的,PyTorch 和 fastai 类并没有什么神奇之处。它们只是方便的预打包部件,使您的生活变得更轻松!(它们还提供了许多我们将在未来章节中使用的额外功能。)
有了这些类,我们现在可以用神经网络替换我们的线性模型。
添加非线性
到目前为止,我们已经有了一个优化函数的一般过程,并且我们已经在一个无聊的函数上尝试了它:一个简单的线性分类器。线性分类器在能做什么方面受到限制。为了使其更复杂一些(并且能够处理更多任务),我们需要在两个线性分类器之间添加一些非线性(即与 ax+b 不同的东西)——这就是给我们神经网络的东西。
这是一个基本神经网络的完整定义:
def simple_net(xb): res = xb@w1 + b1 res = res.max(tensor(0.0)) res = res@w2 + b2 return res
就是这样!在simple_net
中,我们只有两个线性分类器,它们之间有一个max
函数。
在这里,w1
和w2
是权重张量,b1
和b2
是偏置张量;也就是说,这些参数最初是随机初始化的,就像我们在上一节中所做的一样:
w1 = init_params((28*28,30)) b1 = init_params(30) w2 = init_params((30,1)) b2 = init_params(1)
关键点是w1
有 30 个输出激活(这意味着w2
必须有 30 个输入激活,以便匹配)。这意味着第一层可以构建 30 个不同的特征,每个特征代表不同的像素混合。您可以将30
更改为任何您喜欢的数字,以使模型更复杂或更简单。
那个小函数res.max(tensor(0.0))
被称为修正线性单元,也被称为ReLU。我们认为我们都可以同意修正线性单元听起来相当花哨和复杂…但实际上,它不过是res.max(tensor(0.0))
——换句话说,用零替换每个负数。这个微小的函数在 PyTorch 中也可以作为F.relu
使用:
plot_function(F.relu)
Jeremy 说
深度学习中有大量行话,包括修正线性单元等术语。绝大多数这些行话并不比我们在这个例子中看到的一行代码更复杂。事实是,学术界为了发表论文,他们需要让论文听起来尽可能令人印象深刻和复杂。他们通过引入行话来实现这一点。不幸的是,这导致该领域变得比应该更加令人生畏和难以进入。您确实需要学习这些行话,因为否则论文和教程对您来说将毫无意义。但这并不意味着您必须觉得这些行话令人生畏。只需记住,当您遇到以前未见过的单词或短语时,它几乎肯定是指一个非常简单的概念。
基本思想是通过使用更多的线性层,我们的模型可以进行更多的计算,从而模拟更复杂的函数。但是,直接将一个线性布局放在另一个线性布局之后是没有意义的,因为当我们将事物相乘然后多次相加时,可以用不同的事物相乘然后只相加一次来替代!也就是说,一系列任意数量的线性层可以被替换为具有不同参数集的单个线性层。
但是,如果我们在它们之间放置一个非线性函数,比如max
,这就不再成立了。现在每个线性层都有点解耦,可以做自己有用的工作。max
函数特别有趣,因为它作为一个简单的if
语句运行。
Sylvain 说
数学上,我们说两个线性函数的组合是另一个线性函数。因此,我们可以堆叠任意多个线性分类器在一起,而它们之间没有非线性函数,这将与一个线性分类器相同。
令人惊讶的是,可以数学证明这个小函数可以解决任何可计算问题,只要你能找到w1
和w2
的正确参数,并且使这些矩阵足够大。对于任何任意波动的函数,我们可以将其近似为一堆连接在一起的线条;为了使其更接近波动函数,我们只需使用更短的线条。这被称为通用逼近定理。我们这里的三行代码被称为层。第一和第三行被称为线性层,第二行代码被称为非线性或激活函数。
就像在前一节中一样,我们可以利用 PyTorch 简化这段代码:
simple_net = nn.Sequential( nn.Linear(28*28,30), nn.ReLU(), nn.Linear(30,1) )
nn.Sequential
创建一个模块,依次调用列出的每个层或函数。
nn.ReLU
是一个 PyTorch 模块,与F.relu
函数完全相同。大多数可以出现在模型中的函数也有相同的模块形式。通常,只需将F
替换为nn
并更改大小写。在使用nn.Sequential
时,PyTorch 要求我们使用模块版本。由于模块是类,我们必须实例化它们,这就是为什么在这个例子中看到nn.ReLU
。
因为nn.Sequential
是一个模块,我们可以获取它的参数,它将返回它包含的所有模块的所有参数的列表。让我们试一试!由于这是一个更深层的模型,我们将使用更低的学习率和更多的周期:
learn = Learner(dls, simple_net, opt_func=SGD, loss_func=mnist_loss, metrics=batch_accuracy)
learn.fit(40, 0.1)
我们这里不展示 40 行输出,以节省空间;训练过程记录在learn.recorder
中,输出表存储在values
属性中,因此我们可以绘制训练过程中的准确性:
plt.plot(L(learn.recorder.values).itemgot(2));
我们可以查看最终的准确性:
learn.recorder.values[-1][2]
0.982826292514801
在这一点上,我们有一些非常神奇的东西:
- 给定正确的参数集,可以解决任何问题到任何精度的函数(神经网络)
- 找到任何函数的最佳参数集的方法(随机梯度下降)
这就是为什么深度学习可以做出如此奇妙的事情。相信这些简单技术的组合确实可以解决任何问题是我们发现许多学生必须迈出的最大步骤之一。这似乎太好了,以至于难以置信——事情肯定应该比这更困难和复杂吧?我们的建议是:试一试!我们刚刚在 MNIST 数据集上尝试了一下,你已经看到了结果。由于我们自己从头开始做所有事情(除了计算梯度),所以你知道背后没有隐藏任何特殊的魔法。
更深入地探讨
我们不必止步于只有两个线性层。我们可以添加任意数量的线性层,只要在每对线性层之间添加一个非线性。然而,正如您将了解的那样,模型变得越深,实际中优化参数就越困难。在本书的后面,您将学习一些简单但非常有效的训练更深层模型的技巧。
我们已经知道,一个带有两个线性层的单个非线性足以逼近任何函数。那么为什么要使用更深的模型呢?原因是性能。通过更深的模型(具有更多层),我们不需要使用太多参数;事实证明,我们可以使用更小的矩阵,更多的层,获得比使用更大的矩阵和少量层获得更好的结果。
这意味着我们可以更快地训练模型,并且它将占用更少的内存。在 1990 年代,研究人员如此专注于通用逼近定理,以至于很少有人尝试超过一个非线性。这种理论但不实际的基础阻碍了该领域多年。然而,一些研究人员确实尝试了深度模型,并最终能够证明这些模型在实践中表现得更好。最终,出现了理论结果,解释了为什么会发生这种情况。今天,几乎不可能找到任何人只使用一个非线性的神经网络。
当我们使用与我们在第一章中看到的相同方法训练一个 18 层模型时会发生什么:
dls = ImageDataLoaders.from_folder(path) learn = cnn_learner(dls, resnet18, pretrained=False, loss_func=F.cross_entropy, metrics=accuracy) learn.fit_one_cycle(1, 0.1)
时代 | 训练损失 | 验证损失 | 准确性 | 时间 |
0 | 0.082089 | 0.009578 | 0.997056 | 00:11 |
近乎 100%的准确性!这与我们简单的神经网络相比有很大的差异。但是在本书的剩余部分中,您将学习到一些小技巧,可以让您自己从头开始获得如此出色的结果。您已经了解了关键的基础知识。 (当然,即使您知道所有技巧,您几乎总是希望使用 PyTorch 和 fastai 提供的预构建类,因为它们可以帮助您省去自己考虑所有细节的麻烦。)
术语回顾
恭喜:您现在知道如何从头开始创建和训练深度神经网络了!我们经历了很多步骤才达到这一点,但您可能会惊讶于它实际上是多么简单。
既然我们已经到了这一点,现在是一个很好的机会来定义和回顾一些术语和关键概念。
神经网络包含很多数字,但它们只有两种类型:计算的数字和这些数字计算出的参数。这给我们学习最重要的两个术语:
激活
计算的数字(线性和非线性层)
参数
随机初始化并优化的数字(即定义模型的数字)
在本书中,我们经常谈论激活和参数。请记住它们具有特定的含义。它们是数字。它们不是抽象概念,而是实际存在于您的模型中的具体数字。成为一名优秀的深度学习从业者的一部分是习惯于查看您的激活和参数,并绘制它们以及测试它们是否正确运行的想法。
我们的激活和参数都包含在 张量 中。这些只是正规形状的数组—例如,一个矩阵。矩阵有行和列;我们称这些为 轴 或 维度。张量的维度数是它的 等级。有一些特殊的张量:
- 等级-0:标量
- 等级-1:向量
- 等级-2:矩阵
神经网络包含多个层。每一层都是线性或非线性的。我们通常在神经网络中交替使用这两种类型的层。有时人们将线性层及其后续的非线性一起称为一个单独的层。是的,这很令人困惑。有时非线性被称为激活函数。
表 4-1 总结了与 SGD 相关的关键概念。
表 4-1. 深度学习词汇表
术语 | 意义 |
ReLU | 对负数返回 0 且不改变正数的函数。 |
小批量 | 一小组输入和标签,聚集在两个数组中。在这个批次上更新梯度下降步骤(而不是整个 epoch)。 |
前向传播 | 将模型应用于某些输入并计算预测。 |
损失 | 代表我们的模型表现如何(好或坏)的值。 |
梯度 | 损失相对于模型某个参数的导数。 |
反向传播 | 计算损失相对于所有模型参数的梯度。 |
梯度下降 | 沿着梯度相反方向迈出一步,使模型参数稍微变得更好。 |
学习率 | 当应用 SGD 更新模型参数时我们所采取的步骤的大小。 |
选择你的冒险 提醒
在你兴奋地想要窥探内部机制时,你选择跳过第 2 和第三章节了吗?好吧,这里提醒你现在回到第二章,因为你很快就会需要了解那些内容!
问卷调查
- 灰度图像在计算机上是如何表示的?彩色图像呢?
MNIST_SAMPLE
数据集中的文件和文件夹是如何结构化的?为什么?- 解释“像素相似性”方法如何工作以对数字进行分类。
- 什么是列表推导?现在创建一个从列表中选择奇数并将其加倍的列表推导。
- 什么是秩-3 张量?
- 张量秩和形状之间有什么区别?如何从形状中获取秩?
- RMSE 和 L1 范数是什么?
- 如何才能比 Python 循环快几千倍地一次性对数千个数字进行计算?
- 创建一个包含从 1 到 9 的数字的 3×3 张量或数组。将其加倍。选择右下角的四个数字。
- 广播是什么?
- 度量通常是使用训练集还是验证集计算的?为什么?
- SGD 是什么?
- 为什么 SGD 使用小批量?
- SGD 在机器学习中有哪七个步骤?
- 我们如何初始化模型中的权重?
- 什么是损失?
- 为什么我们不能总是使用高学习率?
- 什么是梯度?
- 你需要知道如何自己计算梯度吗?
- 为什么我们不能将准确率作为损失函数使用?
- 绘制 Sigmoid 函数。它的形状有什么特别之处?
- 损失函数和度量之间有什么区别?
- 使用学习率计算新权重的函数是什么?
DataLoader
类是做什么的?- 编写伪代码,显示每个 epoch 中 SGD 所采取的基本步骤。
- 创建一个函数,如果传递两个参数
[1,2,3,4]
和'abcd'
,则返回[(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]
。该输出数据结构有什么特别之处? - PyTorch 中的
view
是做什么的? - 神经网络中的偏差参数是什么?我们为什么需要它们?
- Python 中的
@
运算符是做什么的? backward
方法是做什么的?- 为什么我们必须将梯度清零?
- 我们需要向
Learner
传递什么信息? - 展示训练循环的基本步骤的 Python 或伪代码。
- ReLU 是什么?为值从
-2
到+2
绘制一个图。 - 什么是激活函数?
F.relu
和nn.ReLU
之间有什么区别?- 通用逼近定理表明,任何函数都可以使用一个非线性逼近得到所需的精度。那么为什么我们通常使用更多的非线性函数?
进一步研究
- 从头开始创建自己的
Learner
实现,基于本章展示的训练循环。 - 使用完整的 MNIST 数据集完成本章的所有步骤(不仅仅是 3 和 7)。这是一个重要的项目,需要花费相当多的时间来完成!您需要进行一些研究,以找出如何克服在途中遇到的障碍。