0%

傻子的git教程

tip:我现在只会git clone 和git push 应该也就是和傻子差不多了哈哈哈哈!

安装

安装很简单我就不多说了,就算安装过程中选错了选项也没关系后面都可以在配置文件中改官方教程

验证安装:

  1. 打开vscode即可验证
  2. 终端运行 git —version

    初次配置

官方教程

点开桌面上多出的神奇图标:

pic_git

在他跳出的命令行进行对git的操作:

git_win

1
2
$ git config --global user.name "yourname"
$ git config --global user.email youremail

在本地创建一个git项目

  1. 新建一个文件夹

  2. 在文件夹中创建一个index.html

index

  1. 在vscode中打开文件夹此时会发现vscode的左边栏中的git图标点开后会发生变化。

git_vs

如果没有相关的库我们需要初始化资源也就是第一个选项,打开之后如下:

commit

上传成功但是我并不是这种傻子我需要学习更细的git操作。

这些都是本地操作。

下一步上传github

GitHub

登录github并授权但是我们会发现可能无法正常上传就算用了魔法上网也不行这时候就需要设置代理。

打开终端在终端中输入:

1
2
3

git config --global http.proxy http://proxyuser:proxypwd@proxy.server.com:8080

一般来说可能会没用这个时候输入

1
2
git config --global --unset http.proxy
git config --global --unset https.proxy

取消代理

取消代理之后反而成功了现在出现了两个图标:
icon

蓝色的main代表我们当前项目所处的位置,粉色的云代表远程仓库当前的位置。

branch diff

在本地目录下的命令行输入

1
git log 

可以查看存档所有提交的版本

常见操作更正

一般情况下我们并不会在本地创建仓库在发送到github,我们一般是已有github库后再本地进行操作。

  1. 在github中新建仓库,写上你想要的名字,选择公开或私有

  2. 然后操作和之前一样但是在上传之前打开选项卡选择remote将本地的仓库连接到线上的仓库

remote

和傻子一起写代码

快使用.gitignore

.gitignore的作用是将我们不需要上传的文件排除出去(我是呆瓜实习生的时候就将有几千个文件的devel和build上传了搞得项目一团糟)他非常的重要!

只要在.gitignore中稍加添加你就可以把不必要的东西排除在上传范围之中。

未在.gitignore中写入env和personal_data.yaml:
noignore
写入之后:
ignore

正常上传,如果你要链接新的远程仓库会提示给你需要提供remote name 这个东西是在你同一个本地程序连接到多个远程仓库时区分不同远程仓库用的。

拉人

在设置的general的collaborators中添加需要协同的人就好了。

拉取最新代码

需要注意

getcode

首先点击从所有远程库中抓取将远程库的代码抓取到本地

然后再点击拉取才能将本地的代码更新到远程端版本

pullcode

设置提交门槛不要让什么奇奇怪怪的东西都提交上来

在github的仓库页面的rules中添加new ruleset

  1. 添加规则名称

  2. 启用规则

  3. 添加限制的代码分支

  4. 一般来说打开合并代码时需要提交请求

  5. 升级私有项目到团队组织项目才能生效(需要充钱升级)

想要提交就必须

  1. 创建自己的新分支,并提交到分支

  2. 在github上请求一个pr(pull requests)

  3. 等待审查

(但是你作为仓库的创建人也会被限制)

受够了nomachine和to desk连接远程服务器快使用remote ssh

  1. 安装remote-ssh扩展

  2. 查看你需要远程的电脑的ip地址然后记得输入正确的用户名和密码

如果连接失败很可能是没有安装ssh服务在被控电脑上输入 sudo apt-get install openssh-server安装相关服务就好了。

两种启动方式:

  • 你可以选择在cmd终端中直接输入 ssh username@ipv4后输入对应账户密码启动

    sshcmd

  • 你也可以在vscode中的ssh config保存你的相关远程电脑配置后直接点击进行连接(更方面但是因为某些原因更容易失败)

    vscodessh

问题接踵而至虽然ssh很快很强但是他只有终端可以使用看来是时候告别图形化操作界面了

学习一些ubuntu的操作指令

当你看到黑乎乎没有任何内容的命令行时,不要脑袋空空,你可以在里面进行图形化界面能够进行的任何事情,让我们从简单的开始。

  1. 现在当务之急是要知道你在哪里,快使用pwd获取你现在的位置:

    vscodepwd

  2. 知道了你在哪里接下来我们就需要知道自己手头有什么,用ls查看吧,或者是用llls -l -a可以达到查看所有文件包括隐藏文件的效果。
    vscodels
    vscodell

  3. 查看根目录cd /其中比较关键的一些是bin,etc,sys这些目录,保存了ubuntu系统的一些关键信息。
    rootm

  4. 创建目录mkdir yourfilename进行目录创建:
    mkdir

  5. 我们可以在文件夹里创建我们自己需要的东西,例如像现在我将创建一个markdown文件,输入vi yourfilename.md去创建,然后会自动进行vi进行查看,按下i进入编辑模式在里面进行编辑,然后按下esc输入:wq(其中w代表写入,q代表保存)继成功编写,你可以利用cat yourflie在命令行中简单查看内容。
    从最简单的开始让我们打开一个源码文件看一下,使用vim youfilename.py/.cpp/打开:
    vi

    相同的操作我们来写个python和cpp吧
    vipy
    vicpp

  6. 现在是重头戏,总所周知写代码是不可能写代码的,程序员最重要的技能就是copy和paste。使用cp youwanttocopything newnameforyoupaste,复制文件夹就比较复杂,首先sudo mv 文件名 目标文件夹路径(前提:必须在要移动的文件的目录下执行该命令,而且该文件夹下不能有文件夹,我的part2文件夹放在home目录下),复制一个文件夹(该文件下可以有文件夹)到另一个文件夹下sudo cp -r 文件名 目标文件夹路径
    cp

  7. 修改文件名mv originname targername:
    mv

  8. 删除文件rm yourflie 删除目录rm -r listname:
    rm

接受vim告别vscode

虽然我不太可能用vim进行一个项目的开发但是当我们需要在服务器上进行调试的时候我们还是需要使用vim浅浅的查看和修改一下我们的代码这是必须的(你不能指望在ssh给的命令行里使用.code打开任何东西)

其他计算机视觉问题

上一章学习了实践中训练模型的一些基本技术,例如:学习率和周期律的选择。这一章我们需要深入学习其他两种机器视觉问题。

  • 多标签分类:发生在你想要预测每个图像的多个标签(有时甚至没有标签)

  • 回归: 你的标签是一个或多个数字——数量而不是类别。

这需要深入学习:输出激活,目标和损失函数。

多标签分类

多标签分类指的是识别图像中可能不只包含一种对象类别的问题。可能有多种对象,或者在你寻找的类别中根本没有对象。

tip:模型架构与前一章没有大改变,只有损失函数改变了。

数据

对于我们的示例,我们将使用 PASCAL 数据集,该数据集中的每个图像可以有多种分类对象。

我们首先按照通常的方式下载和提取数据集:

1
2
from fastai.vision.all import *
path = untar_data(URLs.PASCAL_2007)

这个数据集与我们之前看到的不同,它不是按文件名或文件夹结构化的,而是附带一个 CSV 文件,告诉我们每个图像要使用的标签。我们可以通过将其读入 Pandas DataFrame 来检查 CSV 文件:

1
2
df = pd.read_csv(path/'train.csv')
df.head()
文件名 标签 是否有效
0 000005.jpg 椅子 True
1 000007.jpg 汽车 True
2 000009.jpg 马 人 True
3 000012.jpg 汽车 False
4 000016.jpg 自行车 True

正如你所看到的,每个图像中的类别列表显示为一个以空格分隔的字符串。

既然我们已经看到了数据的样子,让我们准备好进行模型训练。

构建数据块

DataFrame对象转换为DataLoaders对象:使用数据块API 来创建DataLoaders对象,因为它提供了灵活性和简单性的良好组合。

以该数据集为例,展示使用数据块 API 构建DataLoaders对象的实践步骤。

数据集

返回单个项目的独立变量和依赖变量的元组的集合

数据加载器

提供一系列小批量的迭代器,其中每个小批量是一批独立变量和一批因变量的组合

除此之外,fastai 还提供了两个类来将您的训练和验证集合在一起:

Datasets

包含一个训练Dataset和一个验证Dataset的迭代器

DataLoaders

包含一个训练DataLoader和一个验证DataLoader的对象

由于DataLoader是建立在Dataset之上并为其添加附加功能(将多个项目整合成一个小批量),通常最容易的方法是首先创建和测试Datasets,然后再查看DataLoaders

当我们创建DataBlock时,我们逐步逐步构建,并使用笔记本检查我们的数据。这是一个很好的方式,可以确保您在编码时保持动力,并留意任何问题。易于调试,因为您知道如果出现问题,它就在您刚刚输入的代码行中!

让我们从没有参数创建的数据块开始,这是最简单的情况:

1
dblock = DataBlock()

我们可以从中创建一个Datasets对象。唯一需要的是一个源——在这种情况下是我们的 DataFrame:

1
dsets = dblock.datasets(df)

这包含一个train和一个valid数据集,我们可以对其进行索引:

1
dsets.train[0]
1
2
3
4
5
6
7
8
(fname       008663.jpg
labels car person
is_valid False
Name: 4346, dtype: object,
fname 008663.jpg
labels car person
is_valid False
Name: 4346, dtype: object)

正如您所看到的,这只是简单地两次返回 DataFrame 的一行。这是因为默认情况下,数据块假定我们有两个东西:输入和目标。我们需要从 DataFrame 中获取适当的字段,可以通过传递get_xget_y函数来实现:

1
2
3
dblock = DataBlock(get_x = lambda r: r['fname'], get_y = lambda r: r['labels'])
dsets = dblock.datasets(df)
dsets.train[0]
1
('005620.jpg', 'aeroplane')

正如您所看到的,我们并没有以通常的方式定义函数,而是使用了 Python 的lambda关键字。这只是定义并引用函数的一种快捷方式。以下更冗长的方法是相同的:

1
2
3
4
5
def get_x(r): return r['fname']
def get_y(r): return r['labels']
dblock = DataBlock(get_x = get_x, get_y = get_y)
dsets = dblock.datasets(df)
dsets.train[0]
1
('002549.jpg', 'tvmonitor')

tip:Lambda 函数非常适合快速迭代,但不兼容序列化,因此我们建议您在训练后要导出您的Learner时使用更冗长的方法(如果您只是在尝试实验,lambda 是可以的)。

我们可以看到独立变量需要转换为完整路径,以便我们可以将其作为图像打开,而因变量需要根据空格字符(这是 Python 的split函数的默认值)进行拆分,以便它变成一个列表:

1
2
3
4
5
def get_x(r): return path/'train'/r['fname']
def get_y(r): return r['labels'].split(' ')
dblock = DataBlock(get_x = get_x, get_y = get_y)
dsets = dblock.datasets(df)
dsets.train[0]
1
2
(Path('/home/sgugger/.fastai/data/pascal_2007/train/008663.jpg'),
['car', 'person'])

要实际打开图像并将其转换为张量,我们需要使用一组转换;块类型将为我们提供这些。我们可以使用先前使用过的相同块类型,只有一个例外:ImageBlock将再次正常工作,因为我们有一个指向有效图像的路径,但CategoryBlock不会起作用。问题在于该块返回一个单个整数,但我们需要为每个项目有多个标签。为了解决这个问题,我们使用MultiCategoryBlock。这种类型的块期望接收一个字符串列表,就像我们在这种情况下所做的那样,所以让我们来测试一下:

1
2
3
4
dblock = DataBlock(blocks=(ImageBlock, MultiCategoryBlock),
get_x = get_x, get_y = get_y)
dsets = dblock.datasets(df)
dsets.train[0]
1
2
3
(PILImage mode=RGB size=500x375,
TensorMultiCategory([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0.,
> 0., 0., 0., 0., 0., 0.]))

正如您所看到的,我们的类别列表的编码方式与常规的CategoryBlock不同。在那种情况下,我们有一个整数表示哪个类别存在,基于它在我们的词汇表中的位置。然而,在这种情况下,我们有一系列 0,其中任何位置上有一个 1 表示该类别存在。例如,如果第二和第四位置上有一个 1,那意味着词汇项二和四在这个图像中存在。这被称为独热编码。我们不能简单地使用类别索引列表的原因是每个列表的长度都不同,而 PyTorch 需要张量,其中所有内容必须是相同长度。

专业术语: 独热编码

使用一个 0 向量,其中每个位置都表示数据中表示的位置,以编码一个整数列表。

使用torch.where函数表明这个例子中的类代表什么。

1
2
idxs = torch.where(dsets.train[0][1]==1.)[0]
dsets.train.vocab[idxs]

1
(#1) ['dog']

使用 NumPy 数组、PyTorch 张量和 fastai 的L类,我们可以直接使用列表或向量进行索引,这使得很多代码(比如这个例子)更清晰、更简洁。

到目前为止,我们忽略了列is_valid,这意味着DataBlock一直在使用默认的随机拆分。要明确选择我们验证集的元素,我们需要编写一个函数并将其传递给splitter(或使用 fastai 的预定义函数或类之一)。它将获取项目(这里是我们整个 DataFrame)并必须返回两个(或更多)整数列表:

1
2
3
4
5
6
7
8
9
10
11
12
def splitter(df):
train = df.index[~df['is_valid']].tolist()
valid = df.index[df['is_valid']].tolist()
return train,valid

dblock = DataBlock(blocks=(ImageBlock, MultiCategoryBlock),
splitter=splitter,
get_x=get_x,
get_y=get_y)

dsets = dblock.datasets(df)
dsets.train[0]
1
2
3
(PILImage mode=RGB size=500x333,
TensorMultiCategory([0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.,
> 0., 0., 0., 0., 0., 0.]))

正如我们讨论过的,DataLoaderDataset中的项目整理成一个小批量。这是一个张量的元组,其中每个张量简单地堆叠了Dataset项目中该位置的项目。

现在我们已经确认了单个项目看起来没问题,还有一步,我们需要确保我们可以创建我们的DataLoaders,即确保每个项目的大小相同。为了做到这一点,我们可以使用RandomResizedCrop

1
2
3
4
5
6
dblock = DataBlock(blocks=(ImageBlock, MultiCategoryBlock),
splitter=splitter,
get_x=get_x,
get_y=get_y,
item_tfms = RandomResizedCrop(128, min_scale=0.35))
dls = dblock.dataloaders(df)

现在我们可以显示我们数据的一个样本:

1
dls.show_batch(nrows=1, ncols=3)

请记住,如果在从DataBlock创建DataLoaders时出现任何问题,或者如果您想查看DataBlock的确切情况,您可以使用我们在上一章中介绍的summary方法。

我们的数据现在已经准备好用于训练模型。正如我们将看到的,当我们创建我们的Learner时,没有任何变化,但在幕后,fastai 库将为我们选择一个新的损失函数:二元交叉熵。

二元交叉熵

创建Learner时其分为四部分:模型,DataLoaders对象,优化器和要使用的损失函数。

本次我们已经构建好了DataLoaders,利用fastai的resnet模型,创建一个SGD优化器,在这章中我们专注于创建一个适合的损失函数。

1
learn = cnn_learner(dls, resnet18)

Learner中的模型通常是从nn.Module继承的类的对象,并且我们可以使用括号调用它,它将返回模型的激活。你应该将独立变量作为一个小批量传递给它。我们可以尝试从我们的DataLoader中获取一个小批量,然后将其传递给模型:

1
2
3
x,y = dls.train.one_batch()
activs = learn.model(x)
activs.shape
1
torch.Size([64, 20])

批量大小为 64,我们需要计算 20 个类别中的每一个的概率。其中一个激活的样子如下:

1
activs[0]
1
2
3
4
tensor([ 2.0258, -1.3543,  1.4640,  1.7754, -1.2820, -5.8053,  3.6130,  0.7193,
> -4.3683, -2.5001, -2.8373, -1.8037, 2.0122, 0.6189, 1.9729, 0.8999,
> -2.6769, -0.3829, 1.2212, 1.6073],
device='cuda:0', grad_fn=<SelectBackward>)

获取模型激活

手动获取一个小批量并将其传递到模型中,并查看激活和损失,对于调试模型非常重要。这对学习也非常有帮助,这样你就可以清楚地看到发生了什么。

它们还没有缩放到 0 到 1 之间,但我们学会了如何在第四章中使用sigmoid函数来做到这一点。我们还看到了如何基于此计算损失——这是我们在第四章中的损失函数,加上了在前一章中讨论的log

1
2
3
def binary_cross_entropy(inputs, targets):
inputs = inputs.sigmoid()
return -torch.where(targets==1, inputs, 1-inputs).log().mean()

请注意,由于我们有一个独热编码的因变量,我们不能直接使用nll_losssoftmax(因此我们不能使用cross_entropy):

  • 正如我们所看到的,softmax要求所有预测总和为 1,并且倾向于使一个激活远远大于其他激活(因为使用了exp);然而,我们可能有多个我们确信出现在图像中的对象,因此限制激活的最大总和为 1 并不是一个好主意。出于同样的原因,如果我们认为任何类别都不出现在图像中,我们可能希望总和小于1。

  • 正如我们所看到的,nll_loss返回的是一个激活值:与项目的单个标签对应的单个激活值。当我们有多个标签时,这是没有意义的。

另一方面,binary_cross_entropy函数,即mnist_loss加上log,正是我们所需要的,这要归功于 PyTorch 的逐元素操作的魔力。每个激活将与每个列的每个目标进行比较,因此我们不必做任何事情使此函数适用于多个列。

Jeremy Says

我真的很喜欢使用像 PyTorch 这样的库,具有广播和逐元素操作,因为我经常发现我可以编写的代码同样适用于单个项目或一批项目,而无需更改。binary_cross_entropy就是一个很好的例子。通过使用这些操作,我们不必自己编写循环,可以依赖 PyTorch 根据我们正在处理的张量的秩适当地执行我们需要的循环。

PyTorch 已经为我们提供了这个函数。实际上,它提供了许多版本,名称相当令人困惑!

F.binary_cross_entropy及其模块等效nn.BCELoss计算一个独热编码目标的交叉熵,但不包括初始的sigmoid。通常,对于独热编码目标,您将希望使用F.binary_cross_entropy_with_logits(或nn.BCEWithLogitsLoss),它们在一个函数中同时执行 sigmoid 和二元交叉熵,就像前面的例子一样。

对于单标签数据集(如 MNIST 或 Pet 数据集),其中目标被编码为单个整数,相应的是F.nll_lossnn.NLLLoss(没有初始 softmax 的版本),以及F.cross_entropynn.CrossEntropyLoss(具有初始 softmax 的版本)。

由于我们有一个独热编码的目标,我们将使用BCEWithLogitsLoss

1
2
3
loss_func = nn.BCEWithLogitsLoss()
loss = loss_func(activs, y)
loss
1
tensor(1.0082, device='cuda:5', grad_fn=<BinaryCrossEntropyWithLogitsBackward>)

我们不需要告诉 fastai 使用这个损失函数(尽管如果我们想要的话可以这样做),因为它将自动为我们选择。fastai 知道DataLoaders具有多个类别标签,因此默认情况下将使用nn.BCEWithLogitsLoss

与前一章相比的一个变化是我们使用的指标:因为这是一个多标签问题,我们不能使用准确度函数。为什么呢?嗯,准确度是这样比较我们的输出和我们的目标的:

1
2
3
4
def accuracy(inp, targ, axis=-1):
"Compute accuracy with `targ` when `pred` is bs * n_classes"
pred = inp.argmax(dim=axis)
return (pred == targ).float().mean()

预测的类是具有最高激活的类(这就是argmax的作用)。这里不起作用,因为我们可能在单个图像上有多个预测。在对我们的激活应用 sigmoid(使它们在 0 和 1 之间)之后,我们需要通过选择阈值来决定哪些是 0,哪些是 1。高于阈值的每个值将被视为 1,低于阈值的每个值将被视为 0:

1
2
3
4
def accuracy_multi(inp, targ, thresh=0.5, sigmoid=True):
"Compute accuracy when `inp` and `targ` are the same size."
if sigmoid: inp = inp.sigmoid()
return ((inp>thresh)==targ.bool()).float().mean()

如果我们直接将accuracy_multi作为指标传递,它将使用threshold的默认值,即 0.5。我们可能希望调整该默认值并创建一个具有不同默认值的新版本的accuracy_multi。为了帮助解决这个问题,Python 中有一个名为partial的函数。它允许我们绑定一个带有一些参数或关键字参数的函数,从而创建该函数的新版本,每当调用它时,总是包含这些参数。例如,这里是一个接受两个参数的简单函数:

1
2
def say_hello(name, say_what="Hello"): return f"{say_what} {name}."
say_hello('Jeremy'),say_hello('Jeremy', 'Ahoy!')
1
('Hello Jeremy.', 'Ahoy! Jeremy.')

我们可以通过使用partial切换到该函数的法语版本:

1
2
f = partial(say_hello, say_what="Bonjour")
f("Jeremy"),f("Sylvain")
1
('Bonjour Jeremy.', 'Bonjour Sylvain.')

现在我们可以训练我们的模型。让我们尝试将准确度阈值设置为 0.2 作为我们的指标:

1
2
learn = cnn_learner(dls, resnet50, metrics=partial(accuracy_multi, thresh=0.2))
learn.fine_tune(3, base_lr=3e-3, freeze_epochs=4)
epoch train_loss valid_loss accuracy_multi time
0 0.903610 0.659728 0.263068 00:07
1 0.724266 0.346332 0.525458 00:07
2 0.415597 0.125662 0.937590 00:07
3 0.254987 0.116880 0.945418 00:07
epoch train_loss valid_loss accuracy_multi time
—- —- —- —- —-
0 0.123872 0.132634 0.940179 00:08
1 0.112387 0.113758 0.949343 00:08
2 0.092151 0.104368 0.951195 00:08

选择阈值很重要。如果选择的阈值太低,通常会选择错误标记的对象。我们可以通过改变我们的度量标准然后调用validate来看到这一点,它会返回验证损失和度量标准:

1
2
learn.metrics = partial(accuracy_multi, thresh=0.1)
learn.validate()
1
(#2) [0.10436797887086868,0.93057781457901]

如果选择的阈值太高,将只选择模型非常有信心的对象:

1
2
learn.metrics = partial(accuracy_multi, thresh=0.99)
learn.validate()
1
(#2) [0.10436797887086868,0.9416930675506592]

我们可以通过尝试几个级别并查看哪个效果最好来找到最佳阈值。如果我们只抓取一次预测,这将快得多:

1
preds,targs = learn.get_preds()

然后我们可以直接调用度量标准。请注意,默认情况下,get_preds会为我们应用输出激活函数(在本例中为 sigmoid),因此我们需要告诉accuracy_multi不要应用它:

1
accuracy_multi(preds, targs, thresh=0.9, sigmoid=False)
1
TensorMultiCategory(0.9554)

现在我们可以使用这种方法找到最佳阈值水平:

1
2
3
xs = torch.linspace(0.05,0.95,29)
accs = [accuracy_multi(preds, targs, thresh=i, sigmoid=False) for i in xs]
plt.plot(xs,accs);

在这种情况下,我们使用验证集来选择一个超参数(阈值),这就是验证集的目的。有时学生们表达了他们的担忧,即我们可能会对验证集过拟合,因为我们正在尝试很多值来找出哪个是最好的。然而,正如你在图中看到的,改变阈值在这种情况下会产生一个平滑的曲线,因此我们显然没有选择不合适的异常值。这是一个很好的例子,说明你必须小心理论(不要尝试很多超参数值,否则可能会过拟合验证集)与实践(如果关系是平滑的,这样做是可以的)之间的区别。

回归

很容易将深度学习模型视为被分类到领域中,如计算机视觉NLP等等。事实上,这就是 fastai 对其应用程序进行分类的方式——主要是因为大多数人习惯于这样思考事物。

但实际上,这隐藏了一个更有趣和更深入的视角。一个模型由其独立和依赖变量以及其损失函数定义。这意味着实际上有比简单的基于领域的分割更广泛的模型数组。也许我们有一个独立变量是图像,一个依赖变量是文本(例如,从图像生成标题);或者我们有一个独立变量是文本,一个依赖变量是图像(例如,从标题生成图像——这实际上是深度学习可以做到的!);或者我们有图像、文本和表格数据作为独立变量,我们试图预测产品购买……可能性真的是无穷无尽的。

要能够超越固定应用程序,为新问题制定自己的新颖解决方案,真正理解数据块 API(也许还有我们将在本书后面看到的中间层 API)是有帮助的。举个例子,让我们考虑图像回归的问题。这指的是从一个独立变量是图像,依赖变量是一个或多个浮点数的数据集中学习。通常我们看到人们将图像回归视为一个完全独立的应用程序——但正如你在这里看到的,我们可以将其视为数据块 API 上的另一个 CNN。

我们将直接跳到图像回归的一个有点棘手的变体,因为我们知道你已经准备好了!我们将做一个关键点模型。关键点指的是图像中表示的特定位置——在这种情况下,我们将使用人物的图像,并且我们将寻找每个图像中人脸的中心。这意味着我们实际上将为每个图像预测两个值:人脸中心的行和列。

数据组装

我们将在这一部分使用Biwi Kinect Head Pose 数据集。我们将像往常一样开始下载数据集:

1
path = untar_data(URLs.BIWI_HEAD_POSE)

让我们看看我们有什么!

1
path.ls()
1
2
(#50) [Path('13.obj'),Path('07.obj'),Path('06.obj'),Path('13'),Path('10'),Path('
> 02'),Path('11'),Path('01'),Path('20.obj'),Path('17')...]

有 24 个从 01 到 24 编号的目录(它们对应不同的被摄人物),以及每个目录对应的.obj文件(我们这里不需要)。让我们看看其中一个目录的内容:

1
(path/'01').ls()

1
2
3
4
5
(#1000) [Path('01/frame_00281_pose.txt'),Path('01/frame_00078_pose.txt'),Path('0
> 1/frame_00349_rgb.jpg'),Path('01/frame_00304_pose.txt'),Path('01/frame_00207_
> pose.txt'),Path('01/frame_00116_rgb.jpg'),Path('01/frame_00084_rgb.jpg'),Path
> ('01/frame_00070_rgb.jpg'),Path('01/frame_00125_pose.txt'),Path('01/frame_003
> 24_rgb.jpg')...]

在子目录中,我们有不同的帧。每个帧都带有一个图像(_rgb.jpg)和一个姿势文件(_pose.txt)。我们可以使用get_image_files轻松递归获取所有图像文件,然后编写一个函数,将图像文件名转换为其关联的姿势文件:

1
2
3
img_files = get_image_files(path)
def img2pose(x): return Path(f'{str(x)[:-7]}pose.txt')
img2pose(img_files[0])
1
Path('13/frame_00349_pose.txt')

让我们来看看我们的第一张图片:

1
2
im = PILImage.create(img_files[0])
im.shape
1
(480, 640)
1
im.to_thumb(160)

Biwi 数据集网站用于解释与每个图像关联的姿势文本文件的格式,显示头部中心的位置。这些细节对我们来说并不重要,所以我们只会展示我们用来提取头部中心点的函数:

1
2
3
4
5
6
cal = np.genfromtxt(path/'01'/'rgb.cal', skip_footer=6)
def get_ctr(f):
ctr = np.genfromtxt(img2pose(f), skip_header=3)
c1 = ctr[0] * cal[0][0]/ctr[2] + cal[0][2]
c2 = ctr[1] * cal[1][1]/ctr[2] + cal[1][2]
return tensor([c1,c2])

这个函数将坐标作为两个项目的张量返回:

1
get_ctr(img_files[0])
1
tensor([384.6370, 259.4787])

我们可以将此函数传递给DataBlock作为get_y,因为它负责为每个项目标记。我们将将图像调整为其输入大小的一半,以加快训练速度。

一个重要的要点是我们不应该只使用随机分割器。在这个数据集中,同一个人出现在多个图像中,但我们希望确保我们的模型可以泛化到它尚未见过的人。数据集中的每个文件夹包含一个人的图像。因此,我们可以创建一个分割器函数,仅为一个人返回True,从而使验证集仅包含该人的图像。

与以前的数据块示例的唯一区别是第二个块是PointBlock。这是必要的,以便 fastai 知道标签代表坐标;这样,它就知道在进行数据增强时,应该对这些坐标执行与图像相同的增强:

1
2
3
4
5
6
7
8
biwi = DataBlock(
blocks=(ImageBlock, PointBlock),
get_items=get_image_files,
get_y=get_ctr,
splitter=FuncSplitter(lambda o: o.parent.name=='13'),
batch_tfms=[*aug_transforms(size=(240,320)),
Normalize.from_stats(*imagenet_stats)]
)

点和数据增强

我们不知道其他库(除了 fastai)会自动且正确地将数据增强应用于坐标。因此,如果您使用另一个库,可能需要禁用这些问题的数据增强。

在进行任何建模之前,我们应该查看我们的数据以确认它看起来没问题:

1
2
dls = biwi.dataloaders(path)
dls.show_batch(max_n=9, figsize=(8,6))

看起来不错!除了通过视觉查看批次外,还可以查看底层张量(尤其是作为学生;这将有助于澄清您对模型实际看到的内容的理解):

1
2
xb,yb = dls.one_batch()
xb.shape,yb.shape
1
(torch.Size([64, 3, 240, 320]), torch.Size([64, 1, 2]))

确保您了解为什么这些是我们小批量的形状。

这是依赖变量的一个示例行:

1
yb[0]
1
tensor([[0.0111, 0.1810]], device='cuda:5')

正如您所看到的,我们不必使用单独的图像回归应用程序;我们所要做的就是标记数据并告诉 fastai 独立变量和因变量代表什么类型的数据。

创建我们的Learner也是一样的。我们将使用与之前相同的函数,只有一个新参数,然后我们就可以准备训练我们的模型了。

训练模型

像往常一样,我们可以使用cnn_learner来创建我们的Learner。还记得在第一章中我们如何使用y_range告诉 fastai 我们目标的范围吗?我们将在这里做同样的事情(fastai 和 PyTorch 中的坐标始终在-1 和+1 之间重新缩放):

1
learn = cnn_learner(dls, resnet18, y_range=(-1,1))

y_range在 fastai 中使用sigmoid_range实现,其定义如下:

1
def sigmoid_range(x, lo, hi): return torch.sigmoid(x) * (hi-lo) + lo

如果定义了y_range,则将其设置为模型的最终层。花点时间思考一下这个函数的作用,以及为什么它强制模型在范围(lo,hi)内输出激活。

这是它的样子:

1
plot_function(partial(sigmoid_range,lo=-1,hi=1), min=-4, max=4)

我们没有指定损失函数,这意味着我们得到了 fastai 选择的默认值。让我们看看它为我们选择了什么:

1
dls.loss_func
1
FlattenedLoss of MSELoss()

这是有道理的,因为当坐标被用作因变量时,大多数情况下我们可能会尽可能地预测接近某个值;这基本上就是 MSELoss(均方误差损失)所做的。如果你想使用不同的损失函数,你可以通过使用 loss_func 参数将其传递给 cnn_learner

还要注意,我们没有指定任何指标。这是因为均方误差已经是这个任务的一个有用指标(尽管在我们取平方根之后可能更易解释)。

我们可以使用学习率查找器选择一个好的学习率:

1
learn.lr_find()

我们将尝试一个学习率为 2e-2:

1
2
lr = 2e-2
learn.fit_one_cycle(5, lr)
epoch train_loss valid_loss time
0 0.045840 0.012957 00:36
1 0.006369 0.001853 00:36
2 0.003000 0.000496 00:37
3 0.001963 0.000360 00:37
4 0.001584 0.000116 00:36

通常情况下,当我们运行这个时,我们得到的损失大约是 0.0001,这对应于这个平均坐标预测误差:

1
math.sqrt(0.0001)
1
0.01

这听起来非常准确!但是重要的是要用 Learner.show_results 查看我们的结果。左侧是实际(真实)坐标,右侧是我们模型的预测:

1
learn.show_results(ds_idx=1, max_n=3, figsize=(6,8))

令人惊讶的是,仅仅几分钟的计算,我们就创建了一个如此准确的关键点模型,而且没有任何特定领域的应用。这就是在灵活的 API 上构建并使用迁移学习的力量!特别引人注目的是,我们能够如此有效地使用迁移学习,即使在完全不同的任务之间;我们的预训练模型是用来进行图像分类的,而我们对图像回归进行了微调。

结论

在乍一看完全不同的问题(单标签分类、多标签分类和回归)中,我们最终使用相同的模型,只是输出的数量不同。唯一改变的是损失函数,这就是为什么重要的是要仔细检查你是否为你的问题使用了正确的损失函数。

fastai 将自动尝试从您构建的数据中选择正确的损失函数,但如果您使用纯 PyTorch 构建您的 DataLoader,请确保您认真考虑您选择的损失函数,并记住您很可能想要以下内容:

  • nn.CrossEntropyLoss 用于单标签分类

  • nn.BCEWithLogitsLoss 用于多标签分类

  • nn.MSELoss 用于回归

课后题

  1. 多标签分类如何提高熊分类器的可用性?
  • 当图片名中没有熊或是出现的不是熊时他能够进行辨别
  1. 在多标签分类问题中,我们如何对因变量进行编码?
  • 因变量需要根据空格字符(这是 Python 的split函数的默认值)进行拆分,以便它变成一个列表
  1. 如何访问 DataFrame 的行和列,就像它是一个矩阵一样?
  • 访问 DataFrame 行和列:使用 iloc 按整数位置或 loc 按标签索引(如 df.iloc[行,列] 或 df.loc[行标签,列标签])。
  1. 如何从 DataFrame 中按名称获取列?
  • 按名称获取列:通过 df[“列名”] 获取单列(Series),或 df[[“列名”]] 保持 DataFrame 结构,列名特殊时用方括号。
  1. DatasetDataLoader 之间有什么区别?
  • 由于DataLoader是建立在Dataset之上并为其添加附加功能(将多个项目整合成一个小批量),通常最容易的方法是首先创建和测试Datasets,然后再查看DataLoaders
  1. Datasets 对象通常包含什么?
  • 包含一个训练Dataset和一个验证Dataset的迭代器
  1. DataLoaders 对象通常包含什么?
  • 包含一个训练DataLoader和一个验证DataLoader的对象
  1. lambda 在 Python 中是做什么的?
  • 这只是定义并引用函数的一种快捷方式
  1. 如何使用数据块 API 自定义独立变量和因变量的创建方法?
  • 使用数据块 API 时,通过 get_x 和 get_y 参数分别定义处理输入(独立变量)和输出(因变量)的自定义方法,例如:
    DataBlock(get_x=自定义函数, get_y=自定义函数)。
  1. 当使用一个独热编码的目标时,为什么 softmax 不是一个合适的输出激活函数?
  • 因为所有类别概率总和并不为1
  1. 当使用一个独热编码的目标时,为什么 nll_loss 不是一个合适的损失函数?
  • nll_loss返回的是一个激活值:与项目的单个标签对应的单个激活值。当我们有多个标签时,这是没有意义的。
  1. nn.BCELossnn.BCEWithLogitsLoss 之间有什么区别?
  • nn.BCELoss计算一个独热编码目标的交叉熵,但不包括初始的sigmoid

  • nn.BCEWithLogitsLoss它在一个函数中同时执行 sigmoid 和二元交叉熵,就像前面的例子一样。

  1. 为什么在多标签问题中不能使用常规准确率?
  • 在多标签问题中,常规准确率要求 所有标签完全正确匹配,而实际场景中部分标签预测正确即可有效,导致常规准确率过于严格且无法反映模型部分预测的有效性。
  1. 何时可以在验证集上调整超参数?
  • 在验证集上调整超参数是标准做法,但需确保 测试集完全独立且仅用于最终评估,避免调整过程中信息泄漏导致过拟合和评估偏差。
  1. y_range 在 fastai 中是如何实现的?(看看你是否可以自己实现并在不偷看的情况下测试!)
  • 在 fastai 中,y_range 通过 在模型输出层添加 Sigmoid 激活函数并线性缩放到指定范围 实现,适用于回归任务以约束预测值的合理区间(如 (a, b))。
  1. 回归问题是什么?对于这样的问题应该使用什么损失函数?
  • 回归问题是预测连续数值的任务(如价格、温度),常用 均方误差(MSE) 或 平均绝对误差(MAE) 作为损失函数,其中 MSE 是最基础且广泛使用的选择。
  1. 为了确保 fastai 库将相同的数据增强应用于您的输入图像和目标点坐标,您需要做什么?
  • 使用PointBlock并将空间变换(如翻转、旋转)添加到item_tfms,同时确保目标点坐标已归一化,即可使fastai对图像和坐标同步应用相同数据增强。

进一步研究

  1. 阅读关于 Pandas DataFrames 的教程,并尝试一些看起来有趣的方法。查看书籍网站上推荐的教程。

  2. 使用多标签分类重新训练熊分类器。看看你是否可以使其有效地处理不包含任何熊的图像,包括在 Web 应用程序中显示该信息。尝试一张包含两种熊的图像。检查在单标签数据集上使用多标签分类是否会影响准确性。

第五章:图像分类

从本章开始学习深度学习机制,创建架构,从训练中获得最佳结果,加快速度和查看神经网络的内部情况,找到可能的问题并解决他们。

本章目标

我们将从重复第一章中查看的相同基本应用程序开始,但我们将做两件事:

  • 让它们变得更好。

  • 将它们应用于更多类型的数据。

从狗和猫识别到宠物品种识别

下载宠物数据

1
2
from fastai2.vision.all import *
path = untar_data(URLs.PETS)

数据集下载

了解数据布局

现在,如果我们要理解如何从每个图像中提取每只宠物的品种,我们需要了解数据是如何布局的。数据布局的细节是深度学习难题的重要组成部分。数据通常以以下两种方式之一提供:

  • 表示数据项的个别文件,例如文本文档或图像,可能组织成文件夹或具有表示有关这些项信息的文件名

  • 数据表(例如,以 CSV 格式)中的数据,其中每行是一个项目,可能包括文件名,提供表中数据与其他格式(如文本文档和图像)中数据之间的连接

有一些例外情况——特别是在基因组学等领域,可能存在二进制数据库格式或甚至网络流——但总体而言,您将处理的绝大多数数据集将使用这两种格式的某种组合。

要查看数据集中的内容,我们可以使用ls方法:

1
path.ls()
1
(#3) [Path('annotations'),Path('images'),Path('models')]

与文档中结果不同的是现在下载的这个数据集内容如下:
数据集查看

我们可以看到这个数据集为我们提供了imagesannotations目录。数据集的网站告诉我们annotations目录包含有关宠物所在位置而不是它们是什么的信息。

查看images的内容:

1
(path/"images").ls()

1
2
3
4
5
6
(#7394) [Path('images/great_pyrenees_173.jpg'),Path('images/wheaten_terrier_46.j
> pg'),Path('images/Ragdoll_262.jpg'),Path('images/german_shorthaired_3.jpg'),P
> ath('images/american_bulldog_196.jpg'),Path('images/boxer_188.jpg'),Path('ima
> ges/staffordshire_bull_terrier_173.jpg'),Path('images/basset_hound_71.jpg'),P
> ath('images/staffordshire_bull_terrier_37.jpg'),Path('images/yorkshire_terrie
> r_18.jpg')...]

images数据集查看

在 fastai 中,大多数返回集合的函数和方法使用一个名为L的类。这个类可以被认为是普通 Python list类型的增强版本,具有用于常见操作的附加便利。例如,当我们在笔记本中显示这个类的对象时,它会以这里显示的格式显示。首先显示的是集合中的项目数,前面带有#。在前面的输出中,你还会看到列表后面有省略号。这意味着只显示了前几个项目,这是件好事,因为我们不希望屏幕上出现超过 7000 个文件名!

通过检查这些文件名,我们可以看到它们似乎是如何结构化的。每个文件名包含宠物品种,然后是一个下划线(_),一个数字,最后是文件扩展名。我们需要创建一段代码,从单个Path中提取品种。Jupyter 笔记本使这变得容易,因为我们可以逐渐构建出可用的东西,然后用于整个数据集。在这一点上,我们必须小心不要做太多假设。例如,如果你仔细观察,你可能会注意到一些宠物品种包含多个单词,因此我们不能简单地在找到的第一个_字符处中断。为了让我们能够测试我们的代码,让我们挑选出一个这样的文件名:

如何利用好文件名

正则表达式

从这样的字符串中提取信息的最强大和灵活的方法是使用regular expression,也称为regex正则表达式是一种特殊的字符串,用正则表达式语言编写,它指定了一个一般规则,用于决定另一个字符串是否通过测试(即“匹配”正则表达式),并且可能用于从另一个字符串中提取特定部分。在这种情况下,我们需要一个正则表达式从文件名中提取宠物品种。

正则表达式示例:使用findall方法来对fname对象的文件名尝试一个正则表达式。

1
re.findall(r'(.+)_\d+.jpg$', fname.name)#这个正则表达式提取出所有字符,直到最后一个下划线字符,只要后续字符是数字,然后是 JPEG 文件扩展名。
1
['great_pyrenees']

文件名正则表达式

利用RegexLabeller类标记整个数据集,使用了数据块 API,我们在第二章中看到过(实际上,我们几乎总是使用数据块 API——它比我们在第一章中看到的简单工厂方法更灵活)

1
2
3
4
5
6
7
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")

这个DataBlock调用中一个重要的部分是我们以前没有见过的这两行:

1
2
item_tfms=Resize(460),
batch_tfms=aug_transforms(size=224, min_scale=0.75)

使用正则表达式标记数据

这些行实现了一个我们称之为预调整的 fastai 数据增强策略。预调整是一种特殊的图像增强方法,旨在最大限度地减少数据破坏,同时保持良好的性能。

预调整

目的:

  1. 我们需要的图像具有相同的尺寸,这样它们可以整合成张量传递给 GPU。
  2. 我们还希望最小化我们执行的不同增强计算的数量。性能要求表明,我们应该尽可能将我们的增强变换组合成更少的变换(以减少计算数量和损失操作的数量)。

预调整策略:

  1. 将图像调整为相对“大”的尺寸,即明显大于目标训练尺寸。

  2. 将所有常见的增强操作(包括调整大小到最终目标大小)组合成一个,并在 GPU 上一次性执行组合操作,而不是单独执行操作并多次插值。

步骤

  1. 调整大小,创建足够大的图像,使其内部区域有多余的边距,以允许进一步的增强变换而不会产生空白区域
    这个转换通过调整大小为一个正方形,使用一个大的裁剪尺寸来实现。在训练集上,裁剪区域是随机选择的,裁剪的大小被选择为覆盖图像宽度或高度中较小的那个。

  2. GPU 用于所有数据增强,并且所有潜在破坏性操作都一起完成,最后进行单次插值。

训练集上的预调整

这张图片展示了两个步骤:

  1. 裁剪全宽或全高:这在item_tfms中,因此它应用于每个单独的图像,然后再复制到 GPU。它用于确保所有图像具有相同的尺寸。在训练集上,裁剪区域是随机选择的。在验证集上,总是选择图像的中心正方形。

  2. 随机裁剪和增强:这在batch_tfms中,因此它一次在 GPU 上应用于整个批次,这意味着速度快。在验证集上,只有调整大小到模型所需的最终大小。在训练集上,首先进行随机裁剪和任何其他增强。

要在 fastai 中实现此过程,您可以使用Resize作为具有大尺寸的项目转换,以及RandomResizedCrop作为具有较小尺寸的批处理转换。如果在aug_transforms函数中包含min_scale参数,RandomResizedCrop将为您添加,就像在上一节中的DataBlock调用中所做的那样。或者,您可以在初始Resize中使用padsquish而不是crop(默认值)。

图 5-2 显示了一个图像经过缩放、插值、旋转,然后再次插值(这是所有其他深度学习库使用的方法),显示在右侧,以及一个图像经过缩放和旋转作为一个操作,然后插值一次(fastai 方法),显示在左侧。

图 5-2。fastai 数据增强策略(左)与传统方法(右)的比较

您可以看到右侧的图像定义不够清晰,在左下角有反射填充伪影;此外,左上角的草完全消失了。我们发现,在实践中,使用预调整显著提高了模型的准确性,通常也会加快速度。

fastai 库还提供了简单的方法来检查您的数据在训练模型之前的外观,这是一个非常重要的步骤。我们将在下一步中看到这些。

检查和调试 DataBlock

我们永远不能假设我们的代码完美运行。编写DataBlock就像编写蓝图一样。如果您的代码中有语法错误,您将收到错误消息,但是您无法保证您的模板会按照您的意图在数据源上运行。因此,在训练模型之前,您应该始终检查您的数据。

您可以使用show_batch方法来执行此操作:

1
dls.show_batch(nrows=1, ncols=3)

检查一下数据吧,确保图像中的狗和标注的品种相对应。(制作数据集的大概率可能会犯一些小错误,哪怕你也是所以检查一下总没错的)。

如果在构建DataBlock时出现错误,您可能在此步骤之前不会看到它。为了调试这个问题,我们鼓励您使用summary方法。它将尝试从您提供的源创建一个批次,并提供大量细节。此外,如果失败,您将准确地看到错误发生的位置,并且库将尝试为您提供一些帮助。例如,一个常见的错误是忘记使用Resize转换,因此最终得到不同大小的图片并且无法将它们整理成批次。在这种情况下,摘要将如下所示(请注意,自撰写时可能已更改确切文本,但它将给您一个概念):

1
2
3
4
5
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")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Setting-up type transforms pipelines
Collecting items from /home/sgugger/.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

Building one sample
Pipeline: PILBase.create
starting from
/home/sgugger/.fastai/data/oxford-iiit-pet/images/american_bulldog_83.jpg
applying PILBase.create gives
PILImage mode=RGB size=375x500
Pipeline: partial -> Categorize
starting from
/home/sgugger/.fastai/data/oxford-iiit-pet/images/american_bulldog_83.jpg
applying partial gives
american_bulldog
applying Categorize gives
TensorCategory(12)

Final sample: (PILImage mode=RGB size=375x500, TensorCategory(12))

Setting up after_item: Pipeline: ToTensor
Setting up before_batch: Pipeline:
Setting up after_batch: Pipeline: IntToFloatTensor

Building one batch
Applying item_tfms to the first sample:
Pipeline: ToTensor
starting from
(PILImage mode=RGB size=375x500, TensorCategory(12))
applying ToTensor gives
(TensorImage of size 3x500x375, TensorCategory(12))

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, 500, 375]),torch.Size([3, 375, 500]),torch.Size([3, 333, 500]),
torch.Size([3, 375, 500])

您可以看到我们如何收集数据并拆分数据,如何从文件名转换为样本(元组(图像,类别)),然后应用了哪些项目转换以及如何在批处理中无法整理这些样本(因为形状不同)。

一旦您认为数据看起来正确,我们通常建议下一步应该使用它来训练一个简单的模型。我们经常看到人们将实际模型的训练推迟得太久。结果,他们不知道他们的基准结果是什么样的。也许您的问题不需要大量花哨的领域特定工程。或者数据似乎根本无法训练模型。这些都是您希望尽快了解的事情。

对于这个初始测试,我们将使用与第一章中使用的相同简单模型:

1
2
learn = cnn_learner(dls, resnet34, metrics=error_rate)
learn.fine_tune(2)
epoch train_loss valid_loss error_rate time
0 1.491732 0.337355 0.108254 00:18
epoch train_loss valid_loss error_rate time
—- —- —- —- —-
0 0.503154 0.293404 0.096076 00:23
1 0.314759 0.225316 0.066306 00:23

正如我们之前简要讨论过的,当我们拟合模型时显示的表格展示了每个训练周期后的结果。记住,一个周期是对数据中所有图像的完整遍历。显示的列是训练集中项目的平均损失、验证集上的损失,以及我们请求的任何指标——在这种情况下是错误率。

请记住损失是我们决定用来优化模型参数的任何函数。但是我们实际上并没有告诉 fastai 我们想要使用什么损失函数。那么它在做什么呢?fastai 通常会根据您使用的数据和模型类型尝试选择适当的损失函数。在这种情况下,我们有图像数据和分类结果,所以 fastai 会默认使用交叉熵损失

交叉熵损失

交叉熵损失是一个类似于我们在上一章中使用的损失函数,但是(正如我们将看到的)有两个好处:

  • 即使我们的因变量有两个以上的类别,它也能正常工作。

  • 这将导致更快速、更可靠的训练。

要理解交叉熵损失如何处理具有两个以上类别的因变量,我们首先必须了解损失函数看到的实际数据和激活是什么样子的。

查看激活和标签

让我们看看我们模型的激活。要从我们的DataLoaders中获取一批真实数据,我们可以使用one_batch方法:

1
x,y = dls.one_batch()

正如您所见,这返回了因变量和自变量,作为一个小批量。让我们看看我们的因变量中包含什么:

1
y
1
2
3
4
5
TensorCategory([11,  0,  0,  5, 20,  4, 22, 31, 23, 10, 20,  2,  3, 27, 18, 23,
> 33, 5, 24, 7, 6, 12, 9, 11, 35, 14, 10, 15, 3, 3, 21, 5, 19, 14, 12,
> 15, 27, 1, 17, 10, 7, 6, 15, 23, 36, 1, 35, 6,
4, 29, 24, 32, 2, 14, 26, 25, 21, 0, 29, 31, 18, 7, 7, 17],
> device='cuda:5')

实际预测是 37 个介于 0 和 1 之间的概率,总和为 1:

1
len(preds[0]),preds[0].sum()
1
(37, tensor(1.0000))

为了将我们模型的激活转换为这样的预测,我们使用了一个叫做softmax的激活函数

Softmax

在我们的分类模型中,我们在最后一层使用 softmax 激活函数,以确保激活值都在 0 到 1 之间,并且它们总和为 1。

Softmax 类似于我们之前看到的 sigmoid 函数。作为提醒,sigmoid 看起来像这样:

1
plot_function(torch.sigmoid, min=-4,max=4)

我们可以将这个函数应用于神经网络的一个激活列,并得到一个介于 0 和 1 之间的数字列,因此对于我们的最后一层来说,这是一个非常有用的激活函数。

现在想象一下,如果我们希望目标中有更多类别(比如我们的 37 种宠物品种)。这意味着我们需要比单个列更多的激活:我们需要一个激活每个类别。例如,我们可以创建一个预测 3 和 7 的神经网络,返回两个激活,每个类别一个——这将是创建更一般方法的一个很好的第一步。让我们只是使用一些标准差为 2 的随机数(因此我们将randn乘以 2)作为示例,假设我们有六个图像和两个可能的类别(其中第一列代表 3,第二列代表 7):

1
2
acts = torch.randn((6,2))*2
acts
1
2
3
4
5
6
tensor([[ 0.6734,  0.2576],
[ 0.4689, 0.4607],
[-2.2457, -0.3727],
[ 4.4164, -1.2760],
[ 0.9233, 0.5347],
[ 1.0698, 1.6187]])

我们不能直接对这个进行 sigmoid 运算,因为我们得不到行相加为 1 的结果(我们希望 3 的概率加上 7 的概率等于 1):

1
acts.sigmoid()
1
2
3
4
5
6
tensor([[0.6623, 0.5641],
[0.6151, 0.6132],
[0.0957, 0.4079],
[0.9881, 0.2182],
[0.7157, 0.6306],
[0.7446, 0.8346]])

在第四章中,我们的神经网络为每个图像创建了一个单一激活,然后通过sigmoid函数传递。这个单一激活代表了模型对输入是 3 的置信度。二进制问题是分类问题的一种特殊情况,因为目标可以被视为单个布尔值,就像我们在mnist_loss中所做的那样。但是二进制问题也可以在任意数量的类别的分类器的更一般上下文中考虑:在这种情况下,我们碰巧有两个类别。正如我们在熊分类器中看到的,我们的神经网络将为每个类别返回一个激活。

那么在二进制情况下,这些激活实际上表示什么?一对激活仅仅表示输入是 3 还是 7 的相对置信度。总体值,无论它们是高还是低,都不重要,重要的是哪个更高,以及高多少。

我们期望,由于这只是表示相同问题的另一种方式,我们应该能够直接在我们的神经网络的两个激活版本上使用sigmoid。事实上我们可以!我们只需取神经网络激活之间的差异,因为这反映了我们对输入是 3 还是 7 更有把握的程度,然后取其 sigmoid:

1
(acts[:,0]-acts[:,1]).sigmoid()
1
tensor([0.6025, 0.5021, 0.1332, 0.9966, 0.5959, 0.3661])

第二列(它是 7 的概率)将是该值从 1 中减去的值。现在,我们需要一种适用于多于两列的方法。事实证明,这个名为softmax的函数正是这样的:

1
def softmax(x): return exp(x) / exp(x).sum(dim=1, keepdim=True)

术语:指数函数(exp)

定义为e**x,其中e是一个特殊的数字,约等于 2.718。它是自然对数函数的倒数。请注意,exp始终为正,并且增长非常迅速!

让我们检查softmax是否为第一列返回与sigmoid相同的值,以及这些值从 1 中减去的值为第二列:

1
2
sm_acts = torch.softmax(acts, dim=1)
sm_acts
1
2
3
4
5
6
tensor([[0.6025, 0.3975],
[0.5021, 0.4979],
[0.1332, 0.8668],
[0.9966, 0.0034],
[0.5959, 0.4041],
[0.3661, 0.6339]])

softmaxsigmoid的多类别等价物——每当我们有超过两个类别且类别的概率必须加起来为 1 时,我们必须使用它,即使只有两个类别,我们通常也会使用它,只是为了使事情更加一致。我们可以创建其他具有所有激活在 0 和 1 之间且总和为 1 的属性的函数;然而,没有其他函数与我们已经看到是平滑且对称的 sigmoid 函数具有相同的关系。此外,我们很快将看到 softmax 函数与我们将在下一节中看到的损失函数密切配合。

如果我们有三个输出激活,就像在我们的熊分类器中一样,为单个熊图像计算 softmax 看起来会像图 5-3 那样。

熊 softmax 示例

图 5-3. 熊分类器上 softmax 的示例

实际上,这个函数是做什么的呢?取指数确保我们所有的数字都是正数,然后除以总和确保我们将得到一堆加起来等于 1 的数字。指数还有一个很好的特性:如果我们激活中的某个数字略大于其他数字,指数将放大这个差异(因为它呈指数增长),这意味着在 softmax 中,该数字将更接近 1。

直观地,softmax 函数真的想要在其他类别中选择一个类别,因此在我们知道每张图片都有一个明确标签时,训练分类器时是理想的选择。(请注意,在推断过程中可能不太理想,因为有时您可能希望模型告诉您它在训练过程中看到的类别中没有识别出任何一个,并且不选择一个类别,因为它的激活分数略高。在这种情况下,最好使用多个二进制输出列来训练模型,每个列使用 sigmoid 激活。)

Softmax 是交叉熵损失的第一部分,第二部分是对数似然。

对数似然

在上一章中为我们的 MNIST 示例计算损失时,我们使用了这个:

1
2
3
def mnist_loss(inputs, targets):
inputs = inputs.sigmoid()
return torch.where(targets==1, 1-inputs, inputs).mean()

就像我们从 sigmoid 到 softmax 的转变一样,我们需要扩展损失函数,使其能够处理不仅仅是二元分类,还需要能够对任意数量的类别进行分类(在本例中,我们有 37 个类别)。我们的激活,在 softmax 之后,介于 0 和 1 之间,并且对于预测批次中的每一行,总和为 1。我们的目标是介于 0 和 36 之间的整数。

在二元情况下,我们使用torch.whereinputs1-inputs之间进行选择。当我们将二元分类作为具有两个类别的一般分类问题处理时,它变得更容易,因为(正如我们在前一节中看到的)现在有两列包含等同于inputs1-inputs的内容。因此,我们只需要从适当的列中进行选择。让我们尝试在 PyTorch 中实现这一点。对于我们合成的 3 和 7 的示例,假设这些是我们的标签:

1
targ = tensor([0,1,0,1,1,0])

这些是 softmax 激活:

1
sm_acts
1
2
3
4
5
6
tensor([[0.6025, 0.3975],
[0.5021, 0.4979],
[0.1332, 0.8668],
[0.9966, 0.0034],
[0.5959, 0.4041],
[0.3661, 0.6339]])

然后对于每个targ项,我们可以使用它来使用张量索引选择sm_acts的适当列,如下所示:

1
2
idx = range(6)
sm_acts[idx, targ]
1
tensor([0.6025, 0.4979, 0.1332, 0.0034, 0.4041, 0.3661])

为了准确了解这里发生了什么,让我们将所有列放在一起放在一个表中。这里,前两列是我们的激活,然后是目标,行索引,最后是前面代码中显示的结果:

3 7 targ idx loss
0.602469 0.397531 0 0 0.602469
0.502065 0.497935 1 1 0.497935
0.133188 0.866811 0 2 0.133188
0.99664 0.00336017 1 3 0.00336017
0.595949 0.404051 1 4 0.404051
0.366118 0.633882 0 5 0.366118

从这个表中可以看出,最后一列可以通过将targidx列作为索引,指向包含37列的两列矩阵来计算。这就是sm_acts[idx, targ]的作用。

这里真正有趣的是,这种方法同样适用于超过两列的情况。想象一下,如果我们为每个数字(0 到 9)添加一个激活列,然后targ包含从 0 到 9 的数字。只要激活列总和为 1(如果我们使用 softmax,它们将是这样),我们将有一个损失函数,显示我们预测每个数字的准确程度。

我们只从包含正确标签的列中选择损失。我们不需要考虑其他列,因为根据 softmax 的定义,它们加起来等于 1 减去与正确标签对应的激活。因此,使正确标签的激活尽可能高必须意味着我们也在降低其余列的激活。

PyTorch 提供了一个与sm_acts[range(n), targ]完全相同的函数(除了它取负数,因为之后应用对数时,我们将得到负数),称为nll_lossNLL代表负对数似然):

1
-sm_acts[idx, targ]
1
tensor([-0.6025, -0.4979, -0.1332, -0.0034, -0.4041, -0.3661])
1
F.nll_loss(sm_acts, targ, reduction='none')
1
tensor([-0.6025, -0.4979, -0.1332, -0.0034, -0.4041, -0.3661])

尽管它的名字是这样的,但这个 PyTorch 函数并不取对数。我们将在下一节看到原因,但首先,让我们看看为什么取对数会有用。

取对数

在前一节中我们看到的函数作为损失函数效果很好,但我们可以让它更好一些。问题在于我们使用的是概率,概率不能小于 0 或大于 1。这意味着我们的模型不会在乎它是预测 0.99 还是 0.999。确实,这些数字非常接近,但从另一个角度来看,0.999 比 0.99 自信程度高 10 倍。因此,我们希望将我们的数字从 0 到 1 转换为从负无穷到无穷。有一个数学函数可以做到这一点:对数(可用torch.log)。它对小于 0 的数字没有定义,并且如下所示:

1
plot_function(torch.log, min=0,max=4)

“对数”这个词让你想起了什么吗?对数函数有这个恒等式:

1
2
y = b**a
a = log(y,b)

在这种情况下,我们假设log(y,b)返回log y 以 b 为底。然而,PyTorch 并没有这样定义log:Python 中的log使用特殊数字e(2.718…)作为底。

也许对数是您在过去 20 年中没有考虑过的东西。但对于深度学习中的许多事情来说,对数是一个非常关键的数学概念,所以现在是一个很好的时机来刷新您的记忆。关于对数的关键事情是这样的关系:

1
log(a*b) = log(a)+log(b)

当我们以这种格式看到它时,它看起来有点无聊;但想想这实际上意味着什么。这意味着当基础信号呈指数或乘法增长时,对数会线性增加。例如,在地震严重程度的里氏震级和噪音级别的分贝尺中使用。它也经常用于金融图表中,我们希望更清楚地显示复合增长率。计算机科学家喜欢使用对数,因为这意味着可以用加法代替修改,这样可以避免产生计算机难以处理的难以处理的规模。

TIP

对我们的概率取正对数或负对数的平均值(取决于是否是正确或不正确的类)给出了负对数似然损失。在 PyTorch 中,nll_loss假设您已经对 softmax 取了对数,因此不会为您执行对数运算。

请注意一下函数名称

nll_loss中的“nll”代表“负对数似然”,但实际上它根本不进行对数运算!它假设您已经已经进行了对数运算。PyTorch 有一个名为log_softmax的函数,以快速准确的方式结合了logsoftmaxnll_loss设计用于在log_softmax之后使用。

当我们首先进行 softmax,然后对其进行对数似然,这种组合被称为交叉熵损失。在 PyTorch 中,这可以通过nn.CrossEntropyLoss来实现(实际上执行log_softmax然后nll_loss):

1
loss_func = nn.CrossEntropyLoss()

正如您所看到的,这是一个类。实例化它会给您一个像函数一样行为的对象:

1
loss_func(acts, targ)
1
tensor(1.8045)

所有 PyTorch 损失函数都以两种形式提供,刚刚显示的类形式以及在F命名空间中提供的普通函数形式:

1
F.cross_entropy(acts, targ)
1
tensor(1.8045)

两者都可以正常工作,并且可以在任何情况下使用。我们注意到大多数人倾向于使用类版本,并且在 PyTorch 的官方文档和示例中更常见,因此我们也会倾向于使用它。

默认情况下,PyTorch 损失函数取所有项目的损失的平均值。您可以使用reduction='none'来禁用这一点:

1
nn.CrossEntropyLoss(reduction='none')(acts, targ)
1
tensor([0.5067, 0.6973, 2.0160, 5.6958, 0.9062, 1.0048])

模型解释

直接解释损失函数非常困难,因为它们被设计为计算机可以区分和优化的东西,而不是人类可以理解的东西。这就是为什么我们有指标。这些指标不用于优化过程,而只是帮助我们这些可怜的人类理解发生了什么。在这种情况下,我们的准确率已经看起来相当不错!那么我们在哪里犯了错误呢?

我们在第一章中看到,我们可以使用混淆矩阵来查看模型表现好和表现不佳的地方:

1
2
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix(figsize=(12,12), dpi=60)

哦,亲爱的——在这种情况下,混淆矩阵很难阅读。我们有 37 种宠物品种,这意味着在这个巨大矩阵中有 37×37 个条目!相反,我们可以使用most_confused方法,它只显示混淆矩阵中预测错误最多的单元格(这里至少有 5 个或更多):

1
interp.most_confused(min_val=5)
1
2
[('american_pit_bull_terrier', 'staffordshire_bull_terrier', 10),
('Ragdoll', 'Birman', 6)]

由于我们不是宠物品种专家,很难知道这些类别错误是否反映了识别品种时的实际困难。因此,我们再次求助于谷歌。一点点搜索告诉我们,这里显示的最常见的类别错误是即使是专家育种者有时也会对其存在分歧的品种差异。因此,这让我们有些安慰,我们正在走在正确的道路上。

我们似乎有一个良好的基线。现在我们可以做些什么来使它变得更好呢?

改进模型

解释迁移学习以及如何尽可能最好地微调我们的预训练模型,而不破坏预训练权重。

重点在于设置学习率(在fastai中有一个高效快捷的工具)

学习率查找器

学习率过高现象:

1
2
learn = cnn_learner(dls, resnet34, metrics=error_rate)
learn.fine_tune(1, base_lr=0.1)
epoch train_loss valid_loss error_rate time
0 8.946717 47.954632 0.893775 00:20
epoch train_loss valid_loss error_rate time
—- —- —- —- —-
0 7.231843 4.119265 0.954668 00:24

现在问题来了怎么找到合适的学习率?

快使用学习率查找器。他的想法是从一个非常非常小的学习率开始,一个我们永远不会认为它太大而无法处理的学习率。我们用这个学习率进行一个 mini-batch,找到之后的损失,然后按一定百分比增加学习率(例如每次加倍)。然后我们再做另一个 mini-batch,跟踪损失,并再次加倍学习率。我们一直这样做,直到损失变得更糟没有变得更好。然后我们按照要求选择一个比这个点稍低的学习率:

  • 比最小损失达到的地方少一个数量级(即最小值除以 10)

  • 最后一次损失明显减少的点

学习率查找器计算曲线上的这些点来帮助您。这两个规则通常给出大致相同的值。在第一章中,我们没有指定学习率,而是使用了 fastai 库的默认值(即 1e-3)

关于mini-batch:我们已知在梯度下降中需要对所有样本进行处理过后然后走一步,那么如果我们的样本规模的特别大的话效率就会比较低。假如有500万,甚至5000万个样本(在我们的业务场景中,一般有几千万行,有些大数据有10亿行)的话走一轮迭代就会非常的耗时。这个时候的梯度下降叫做full batch。 所以为了提高效率,我们可以把样本分成等量的子集。 例如我们把100万样本分成1000份, 每份1000个样本, 这些子集就称为mini batch。然后我们分别用一个for循环遍历这1000个子集。 针对每一个子集做一次梯度下降。 然后更新参数w和b的值。接着到下一个子集中继续进行梯度下降。 这样在遍历完所有的mini batch之后我们相当于在梯度下降中做了1000次迭代。 我们将遍历一次所有样本的行为叫做一个 epoch,也就是一个世代。 在mini batch下的梯度下降中做的事情其实跟full batch一样,只不过我们训练的数据不再是所有的样本,而是一个个的子集。 这样在mini batch我们在一个epoch中就能进行1000次的梯度下降,而在full batch中只有一次。 这样就大大的提高了我们算法的运行速度。

1
2
learn = cnn_learner(dls, resnet34, metrics=error_rate)
lr_min,lr_steep = learn.lr_find()

1
print(f"Minimum/10: {lr_min:.2e}, steepest point: {lr_steep:.2e}")
1
Minimum/10: 8.32e-03, steepest point: 6.31e-03

我们可以看到在 1e-6 到 1e-3 的范围内,没有什么特别的事情发生,模型不会训练。然后损失开始减少,直到达到最小值,然后再次增加。我们不希望学习率大于 1e-1,因为这会导致训练发散(您可以自行尝试),但 1e-1 已经太高了:在这个阶段,我们已经离开了损失稳定下降的阶段。

在这个学习率图中,看起来学习率约为 3e-3 可能是合适的,所以让我们选择这个:

1
2
learn = cnn_learner(dls, resnet34, metrics=error_rate)
learn.fine_tune(2, base_lr=3e-3)
epoch train_loss valid_loss error_rate time
0 1.071820 0.427476 0.133965 00:19
epoch train_loss valid_loss error_rate time
—- —- —- —- —-
0 0.738273 0.541828 0.150880 00:24
1 0.401544 0.266623 0.081867 00:24

问卷调查

  1. 为什么我们首先在 CPU 上调整大小到较大尺寸,然后在 GPU 上调整到较小尺寸?
  • 先在CPU上放大以高效处理大尺寸插值,再在GPU上缩小以利用并行加速动态增强并节省显存。
  1. 如果您不熟悉正则表达式,请查找正则表达式教程和一些问题集,并完成它们。查看书籍网站以获取建议。
  1. 对于大多数深度学习数据集,数据通常以哪两种方式提供?
  • 表示数据项的个别文件,例如文本文档或图像,可能组织成文件夹或具有表示有关这些项信息的文件名

  • 数据表(例如,以 CSV 格式)中的数据,其中每行是一个项目,可能包括文件名,提供表中数据与其他格式(如文本文档和图像)中数据之间的连接

有一些例外情况——特别是在基因组学等领域,可能存在二进制数据库格式或甚至网络流——但总体而言,您将处理的绝大多数数据集将使用这两种格式的某种组合。

  1. 查阅L的文档,并尝试使用它添加的一些新方法。查阅 Python pathlib模块的文档,并尝试使用Path类的几种方法。
  • 如何找类和模块的文档:问ai最高效pathlib,L文档本地查看:使用 help() 函数help(L)查看文档,print(dir(L))查看所有类和对象。
  1. 给出两个图像转换可能降低数据质量的示例。
  • 许多旋转和缩放操作将需要插值来创建像素。这些插值像素是从原始图像数据派生的,但质量较低。
  1. fastai 提供了哪种方法来查看DataLoaders中的数据?
  • ‘one_batch’的方法
  1. fastai 提供了哪种方法来帮助您调试DataBlock
  • 现在我们确认了正则表达式对示例的有效性,让我们用它来标记整个数据集。fastai 提供了许多类来帮助标记。对于使用正则表达式进行标记,我们可以使用RegexLabeller类。在这个例子中,我们使用了数据块 API,我们在第二章中看到过(实际上,我们几乎总是使用数据块 API——它比我们在第一章中看到的简单工厂方法更灵活):

    1
    2
    3
    4
    5
    6
    7
    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")

    这个DataBlock调用中一个重要的部分是我们以前没有见过的这两行:

    1
    2
    item_tfms=Resize(460),
    batch_tfms=aug_transforms(size=224, min_scale=0.75)

    这些行实现了一个我们称之为预调整的 fastai 数据增强策略。预调整是一种特殊的图像增强方法,旨在最大限度地减少数据破坏,同时保持良好的性能。

  1. 在彻底清理数据之前,是否应该暂停训练模型?
  • 必要的
  1. 在 PyTorch 中,交叉熵损失是由哪两个部分组合而成的?
  • Softmax 是交叉熵损失的第一部分,第二部分是对数似然。
  1. softmax 确保的激活函数的两个属性是什么?为什么这很重要?
  • 以确保激活值都在 0 到 1 之间,并且它们总和为 1。
  1. 何时可能希望激活函数不具有这两个属性?
  • 非二元情况下
  1. 自己计算图 5-3 中的expsoftmax列(即在电子表格、计算器或笔记本中)。
  • exp = exp(output),softmax = exp(output)/count(exp)
  1. 为什么我们不能使用torch.where为标签可能有多于两个类别的数据集创建损失函数?
  • 我们使用torch.whereinputs1-inputs之间进行选择
  1. log(-2)的值是多少?为什么?
  • 不存在吧 这玩意 哪有log负数这玩意的
  1. 选择学习率时有哪两个好的经验法则来自学习率查找器?
  • 从一个非常非常小的学习率开始,一个我们永远不会认为它太大而无法处理的学习率。我们用这个学习率进行一个 mini-batch,找到之后的损失,然后按一定百分比增加学习率(例如每次加倍)。然后我们再做另一个 mini-batch,跟踪损失,并再次加倍学习率。
  1. fine_tune方法执行了哪两个步骤?
  • 训练随机添加的层一个周期,同时冻结所有其他层

  • 解冻所有层,并根据请求的周期数进行训练

  1. 在 Jupyter Notebook 中,如何获取方法或函数的源代码?
  • 在 Jupyter Notebook 中,使用 ?? 符号(如 函数名??)或 inspect.getsource(函数名) 可直接查看方法或函数的源代码。
  1. 什么是区分性学习率?
  • 对神经网络的早期层使用较低的学习率,对后期层(尤其是随机添加的层)使用较高的学习率。
  1. 当将 Python slice对象作为学习率传递给 fastai 时,它是如何解释的?
  • 传递的第一个值将是神经网络最早层的学习率,第二个值将是最后一层的学习率。中间的层将在该范围内等距地乘法地具有学习率。让我们使用这种方法复制先前的训练,但这次我们只将我们网络的最低层的学习率设置为 1e-6;其他层将增加到 1e-4。让我们训练一段时间。
  1. 为什么在使用 1cycle 训练时,提前停止是一个不好的选择?
  • 因为那些中间的 epochs 出现在学习率还没有机会达到小值的情况下,这时它才能真正找到最佳结果。因此,如果你发现你过拟合了,你应该重新从头开始训练模型,并根据之前找到最佳结果的地方选择一个总的 epochs 数量。
  1. resnet50resnet101之间有什么区别?
  • 架构往往只有少数几种变体。例如,在本章中使用的 ResNet 架构有 18、34、50、101 和 152 层的变体,都是在 ImageNet 上预训练的。一个更大的(更多层和参数;有时被描述为模型的容量)ResNet 版本总是能够给我们更好的训练损失,但它可能更容易过拟合,因为它有更多参数可以过拟合。
  1. to_fp16是做什么的?
  • 将训练的数据精度降低提高训练速度。(半精度浮点数,也称为 fp16)

进一步研究

  1. 找到 Leslie Smith 撰写的介绍学习率查找器的论文,并阅读。

  2. 看看是否可以提高本章分类器的准确性。您能达到的最佳准确性是多少?查看论坛和书籍网站,看看其他学生在这个数据集上取得了什么成就以及他们是如何做到的。

这里会记录一些常用的conda指令

首先拿到一台陌生的电脑需要判断电脑上有没有安装conda,conda版本又是多少

1
conda --version

如果遇到下载慢的问题就需要设置镜像解决

1
2
3
4
5
6
7
8
9
10
#设置清华镜像
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/bioconda/
#设置bioconda
conda config --add channels bioconda
conda config --add channels conda-forge
#设置搜索时显示通道地址
conda config --set show_channel_urls yes

tips:事实上之前我查询清华源已经把研究院的ip禁掉了因为有人频繁下载QT

准备好就可以创建虚拟环境了

1
conda create -n env_name python=3.8

激活虚拟环境

1
conda activate env_name

退出虚拟环境

1
conda deactivate
删除虚拟环境
1
conda remove --name env_name --all

删除环境中的某个包

1
conda remove --name env_name  package_name

查看当前拥有的包

1
conda list

安装包的方式

根据我的使用经验可以使用conda安装也可以安装pip进行安装所以总结一下安装的命令

1
conda install package_name
1
pip install package_name

卸载的话就是相应的uninstall

特别说明: pip和conda在安装软件包时,在依赖关系方面的处理机制不同。pip在递归的串行循环中安装依赖项,不会确保同时满足所有软件包的依赖关系,如果按顺序较早安装的软件包相对于按顺序较晚安装的软件包具有不兼容的依赖项版本,则可能导致环境以微妙的方式被破坏掉;conda使用SAT(satisfiability)solver来验证是否满足环境中安装的所有软件包的所有要求,只要有关依赖项的软件包元数据正确,conda就会按预期产生可用的环境。

关于混用问题:不建议混用,不然容易出现错误。

关于包的位置:

  • conda install xxx:这种方式安装的库都会放在anaconda3/pkgs目录下,这样的好处就是,当在某个环境下已经下载好了某个库,再在另一个环境中还需要这个库时,就可以直接从pkgs目录下将该库复制至新环境而不用重复下载。
  • pip install xxx:分两种情况,一种情况就是当前conda环境的python是conda安装的,和系统的不一样,那么xxx会被安装到anaconda3/envs/current_env/lib/python3.x/site-packages文件夹中,如果当前conda环境用的是系统的python,那么xxx会通常会被安装到~/.local/lib/python3.x/site-packages文件夹中

清理 conda 缓存

1
conda clean -h

查看清理的具体细节

变更python版本

1
conda install python=3.5

更新python版本到最新

1
conda update python

第四章:底层:训练数字分类器

在第二章中看到训练各种模型的样子后,现在让我们深入了解并看看究竟发生了什么。我们将使用计算机视觉来介绍深度学习的基本工具和概念。

确切地说,我们将讨论数组和张量的作用以及广播的作用,这是一种使用它们表达性地的强大技术。我们将解释随机梯度下降(SGD),这是通过自动更新权重学习的机制。我们将讨论基本分类任务的损失函数的选择,以及小批量的作用。我们还将描述基本神经网络正在执行的数学。最后,我们将把所有这些部分组合起来。

在未来的章节中,我们还将深入研究其他应用,并看看这些概念和工具如何泛化。但本章是关于奠定基础的。坦率地说,这也使得这是最困难的章节之一,因为这些概念彼此相互依赖。就像一个拱门,所有的石头都需要放在正确的位置才能支撑结构。也像一个拱门,一旦发生这种情况,它就是一个强大的结构,可以支撑其他事物。但是需要一些耐心来组装。

让我们开始吧。第一步是考虑图像在计算机中是如何表示的。

像素:计算机视觉的基础

要理解计算机视觉模型中发生的事情,我们首先必须了解计算机如何处理图像。我们将使用计算机视觉中最著名的数据集之一 MNIST 进行实验。MNIST 包含由国家标准与技术研究所收集的手写数字图像,并由 Yann Lecun 及其同事整理成一个机器学习数据集。Lecun 在 1998 年使用 MNIST 在 LeNet-5 中,这是第一个演示实用手写数字序列识别的计算机系统。这是人工智能历史上最重要的突破之一。

对于这个初始教程,我们只是尝试创建一个模型,可以将任何图像分类为 3 或 7。所以让我们下载一个包含这些数字图像的 MNIST 样本:

1
path = untar_data(URLs.MNIST_SAMPLE)

我们可以使用ls来查看此目录中的内容,这是 fastai 添加的一个方法。这个方法返回一个特殊的 fastai 类L的对象,它具有 Python 内置list的所有功能,还有更多功能。其中一个方便的功能是,在打印时,它会显示项目的计数,然后列出项目本身(如果项目超过 10 个,它只显示前几个):

1
path.ls()
1
2
3
(#9) [Path('cleaned.csv'),Path('item_list.txt'),Path('trained_model.pkl'),Path('
> models'),Path('valid'),Path('labels.csv'),Path('export.pkl'),Path('history.cs
> v'),Path('train')]

MNIST 数据集遵循机器学习数据集的常见布局:训练集和验证(和/或测试)集分开存放。让我们看看训练集中的内容:

1
(path/'train').ls()
1
(#2) [Path('train/7'),Path('train/3')]

有一个包含 3 的文件夹,和一个包含 7 的文件夹。在机器学习术语中,我们说“3”和“7”是这个数据集中的标签(或目标)。让我们看看其中一个文件夹中的内容(使用sorted确保我们都得到相同的文件顺序):

1
2
3
threes = (path/'train'/'3').ls().sorted()
sevens = (path/'train'/'7').ls().sorted()
threes
1
2
3
4
(#6131) [Path('train/3/10.png'),Path('train/3/10000.png'),Path('train/3/10011.pn
> g'),Path('train/3/10031.png'),Path('train/3/10034.png'),Path('train/3/10042.p
> ng'),Path('train/3/10052.png'),Path('train/3/1007.png'),Path('train/3/10074.p
> ng'),Path('train/3/10091.png')...]

正如我们所预期的那样,它充满了图像文件。让我们现在看一个。这是一个手写数字 3 的图像,来自著名的手写数字 MNIST 数据集:

1
2
3
im3_path = threes[1]
im3 = Image.open(im3_path)
im3

在这里,我们使用Python Imaging Library(PIL)中的Image类,这是最广泛使用的 Python 包,用于打开、操作和查看图像。Jupyter 知道 PIL 图像,所以它会自动为我们显示图像。

在计算机中,一切都以数字表示。要查看构成这幅图像的数字,我们必须将其转换为NumPy 数组PyTorch 张量。例如,这是转换为 NumPy 数组后图像的一部分的样子:

1
array(im3)[4:10,4:10]
1
2
3
4
5
6
array([[  0,   0,   0,   0,   0,   0],
[ 0, 0, 0, 0, 0, 29],
[ 0, 0, 0, 48, 166, 224],
[ 0, 93, 244, 249, 253, 187],
[ 0, 107, 253, 253, 230, 48],
[ 0, 3, 20, 20, 15, 0]], dtype=uint8)

4:10表示我们请求从索引 4(包括)到 10(不包括)的行,列也是一样。NumPy 从上到下,从左到右索引,因此此部分位于图像的左上角附近。这里是一个 PyTorch 张量:

1
tensor(im3)[4:10,4:10]
1
2
3
4
5
6
tensor([[  0,   0,   0,   0,   0,   0],
[ 0, 0, 0, 0, 0, 29],
[ 0, 0, 0, 48, 166, 224],
[ 0, 93, 244, 249, 253, 187],
[ 0, 107, 253, 253, 230, 48],
[ 0, 3, 20, 20, 15, 0]], dtype=torch.uint8)

4:10表示我们请求从索引 4(包括)到 10(不包括)的行,列也是一样。NumPy 从上到下,从左到右索引,因此此部分位于图像的左上角附近。这里是一个 PyTorch 张量:

1
tensor(im3)[4:10,4:10]
1
2
3
4
5
6
tensor([[  0,   0,   0,   0,   0,   0],
[ 0, 0, 0, 0, 0, 29],
[ 0, 0, 0, 48, 166, 224],
[ 0, 93, 244, 249, 253, 187],
[ 0, 107, 253, 253, 230, 48],
[ 0, 3, 20, 20, 15, 0]], dtype=torch.uint8)

我们可以切片数组,只选择包含数字顶部部分的部分,然后使用 Pandas DataFrame 使用渐变对值进行着色,这清楚地显示了图像是如何由像素值创建的:

1
2
3
im3_t = tensor(im3)
df = pd.DataFrame(im3_t[4:15,4:22])
df.style.set_properties(**{'font-size':'6pt'}).background_gradient('Greys')

你可以看到,背景白色像素存储为数字 0,黑色为数字 255,灰色在两者之间。整个图像横向包含 28 个像素,纵向包含 28 个像素,总共 768 个像素。(这比你从手机相机得到的图像要小得多,手机相机有数百万像素,但对于我们的初始学习和实验来说,这是一个方便的大小。我们将很快构建更大的全彩图像。)

所以,现在你已经看到了计算机对图像的看法,让我们回顾一下我们的目标:创建一个能够识别 3 和 7 的模型。你会如何让计算机做到这一点呢?

停下来思考!

在继续阅读之前,花点时间考虑一下计算机可能如何识别这两个数字。它可能能够看到什么样的特征?它可能如何识别这些特征?它如何将它们结合起来?学习最好的方式是尝试自己解决问题,而不仅仅是阅读别人的答案;所以离开这本书几分钟,拿一张纸和笔,写下一些想法。

我认为计算机可能会用识别到的手写数字图像的矩阵和标准的数字图像的矩阵进行运算像是点乘或是什么得出一个能给表现图片数字和标准数字相似度的一个数值进行比较取得最大的为识别数字。

第一次尝试:像素相似度

所以,这是一个第一个想法:我们可以找到每个 3 的像素的平均值,然后对 7 做同样的操作。这将给我们两组平均值,定义了我们可能称之为“理想”3 和 7。然后,为了将图像分类为一个数字或另一个数字,我们看看这两个理想数字中图像与哪个更相似。这肯定似乎比没有好,所以这将成为一个很好的基线。

术语:基线

一个简单的模型,你有信心应该表现得相当不错。它应该简单实现和易于测试,这样你就可以测试每个改进的想法,并确保它们始终优于基线。如果没有以合理的基线开始,很难知道你的超级花哨的模型是否好用。创建基线的一个好方法是做我们在这里做的事情:考虑一个简单、易于实现的模型。另一个好方法是四处寻找解决类似问题的其他人,并在你的数据集上下载并运行他们的代码。最好两者都尝试一下!

我们简单模型的第一步是获取我们两组像素值的平均值。在这个过程中,我们将学习很多有趣的 Python 数值编程技巧!

让我们创建一个包含所有 3 的张量堆叠在一起。我们已经知道如何创建包含单个图像的张量。要创建一个包含目录中所有图像的张量,我们将首先使用 Python 列表推导来创建一个单个图像张量的普通列表。

我们将使用 Jupyter 在途中做一些小的检查——在这种情况下,确保返回的项目数量看起来合理:

1
2
3
seven_tensors = [tensor(Image.open(o)) for o in sevens]
three_tensors = [tensor(Image.open(o)) for o in threes]
len(three_tensors),len(seven_tensors)
1
(6131, 6265)

列表推导

列表和字典推导是 Python 的一个很棒的特性。许多 Python 程序员每天都在使用它们,包括本书的作者们——它们是“Python 的成语”。但是来自其他语言的程序员可能以前从未见过它们。许多很棒的教程只需一次网络搜索,所以我们现在不会花很长时间讨论它们。

我来补充一下:
Python 字典(Dictionary)
Python 字典(Dictionary) 字典是另一种可变容器模型,且可存储任意类型对象。 字典的每个键值 key:value 对用冒号 : 分割,每个键值对之间用逗号 , 分割,整个字典包括在花括号 {} 中 ,格式如下所示:
1
d = {key1 : value1, key2 : value2 }
注意:dict 作为 Python 的关键字和内置函数,变量名不建议命名为 dict。 键一般是唯一的,如果重复最后的一个键值对会替换前面的,值不需要唯一。
1
2
3
4
5
>>> tinydict = {'a': 1, 'b': 2, 'b': '3'}
>>> tinydict['b']
'3'
>>> tinydict
{'a': 1, 'b': '3'}
值可以取任何数据类型,但键必须是不可变的,如字符串,数字或元组。 一个简单的字典实例:
1
tinydict = {'Alice': '2341', 'Beth': '9102', 'Cecil': '3258'}
也可如此创建字典:
1
2
tinydict1 = { 'abc': 456 }
tinydict2 = { 'abc': 123, 98.6: 37 }
这里简单介绍一下字典在网页上还有更加详细的介绍和修改方法:[字典](https://www.runoob.com/python/python-dictionary.html)
Python 列表(List)
序列是Python中最基本的数据结构。序列中的每个元素都分配一个数字 - 它的位置,或索引,第一个索引是0,第二个索引是1,依此类推。 Python有6个序列的内置类型,但最常见的是列表和元组。 序列都可以进行的操作包括索引,切片,加,乘,检查成员。 此外,Python已经内置确定序列的长度以及确定最大和最小的元素的方法。 列表是最常用的Python数据类型,它可以作为一个方括号内的逗号分隔值出现。 列表的数据项不需要具有相同的类型 创建一个列表,只要把逗号分隔的不同的数据项使用方括号括起来即可。如下所示:
1
2
3
list1 = ['physics', 'chemistry', 1997, 2000]
list2 = [1, 2, 3, 4, 5 ]
list3 = ["a", "b", "c", "d"]
与字符串的索引一样,列表索引从0开始。列表可以进行截取、组合等。更多对列表的操作:[列表](https://www.runoob.com/python/python-lists.html)

这里有一个快速的解释和示例,让您开始。列表推导看起来像这样:new_list = [f(o) for o in a_list if o>0]。这将返回a_list中大于 0 的每个元素,在将其传递给函数f之后。这里有三个部分:您正在迭代的集合(a_list),一个可选的过滤器(if o>0),以及对每个元素执行的操作(f(o))。不仅写起来更短,而且比用循环创建相同列表的替代方法更快。

我们还将检查其中一张图像是否正常。由于我们现在有张量(Jupyter 默认会将其打印为值),而不是 PIL 图像(Jupyter 默认会显示图像),我们需要使用 fastai 的show_image函数来显示它:

1
show_image(three_tensors[1]);

对于每个像素位置,我们想要计算该像素的强度在所有图像上的平均值。为了做到这一点,我们首先将此列表中的所有图像组合成一个三维张量。描述这样的张量最常见的方式是称之为rank-3 张量。我们经常需要将集合中的单个张量堆叠成一个张量。不出所料,PyTorch 带有一个名为stack的函数,我们可以用它来实现这个目的。

PyTorch 中的一些操作,如取平均值,需要我们将整数类型转换为浮点类型。由于我们稍后会需要这个,我们现在也将我们的堆叠张量转换为float。在 PyTorch 中进行转换就像写下您希望转换为的类型名称,并将其视为方法一样简单。

通常,当图像是浮点数时,像素值应该在 0 到 1 之间,所以我们也会在这里除以 255:

1
2
3
stacked_sevens = torch.stack(seven_tensors).float()/255
stacked_threes = torch.stack(three_tensors).float()/255
stacked_threes.shape
1
torch.Size([6131, 28, 28])

张量最重要的属性也许是其形状。这告诉您每个轴的长度。在这种情况下,我们可以看到我们有 6,131 张图像,每张图像大小为 28×28 像素。关于这个张量没有特别的地方表明第一个轴是图像的数量,第二个是高度,第三个是宽度——张量的语义完全取决于我们以及我们如何构建它。就 PyTorch 而言,它只是内存中的一堆数字。

张量形状的长度是其秩:

1
len(stacked_threes.shape)
1
3

对于您来说,将张量术语的这些部分记忆并加以实践非常重要:是张量中轴或维度的数量;形状是张量每个轴的大小。

关于维度

要小心,因为术语“维度”有时以两种方式使用。考虑我们生活在“三维空间”中,其中物理位置可以用长度为 3 的向量v描述。但根据 PyTorch,属性v.ndim(看起来确实像v的“维度数量”)等于一,而不是三!为什么?因为v是一个向量,它是一个秩为一的张量,这意味着它只有一个(即使该轴的长度为三)。换句话说,有时维度用于描述轴的大小(“空间是三维的”),而其他时候用于描述秩或轴的数量(“矩阵有两个维度”)。当感到困惑时,我发现将所有陈述转换为秩、轴和长度这些明确的术语是有帮助的。

我们也可以直接使用ndim来获取张量的秩:

1
stacked_threes.ndim
1
3

最后,我们可以计算理想的 3 是什么样子的。我们通过沿着我们堆叠的 rank-3 张量的维度 0 取平均值来计算所有图像张量的平均值。这是索引所有图像的维度。

换句话说,对于每个像素位置,这将计算所有图像中该像素的平均值。结果将是每个像素位置的一个值,或者一个单独的图像。这就是它:

1
2
mean3 = stacked_threes.mean(0)
show_image(mean3);

根据这个数据集,这是理想的数字 3!(您可能不喜欢,但这就是顶级数字 3 表现的样子。)您可以看到在所有图像都认为应该是暗的地方非常暗,但在图像不一致的地方变得模糊。

让我们对 7 做同样的事情,但一次将所有步骤放在一起以节省时间:

1
2
mean7 = stacked_sevens.mean(0)
show_image(mean7);

现在让我们选择一个任意的 3,并测量它与我们的“理想数字”的距离

停下来思考一下!

您如何计算特定图像与我们的每个理想数字之间的相似程度?在继续前进之前,请记得远离这本书,记录一些想法!研究表明,通过解决问题、实验和尝试新想法,您参与学习过程时,召回和理解会显著提高。

这是一个示例 3:

1
2
a_3 = stacked_threes[1]
show_image(a_3);

我们如何确定它与我们理想的 3 之间的距离?我们不能简单地将此图像的像素之间的差异相加,并与理想数字进行比较。一些差异将是正的,而另一些将是负的,这些差异将相互抵消,导致一种情况,即在某些地方太暗而在其他地方太亮的图像可能被显示为与理想的总差异为零。那将是误导性的!

为了避免这种情况,数据科学家在这种情况下使用两种主要方法来测量距离:

  • 取差值的绝对值的平均值(绝对值是将负值替换为正值的函数)。这被称为平均绝对差L1 范数

  • 取差值的平方的平均值(使所有值变为正数),然后取平方根(撤销平方)。这被称为均方根误差(RMSE)或L2 范数

在pytorch中 取差值的绝对值的平均值(绝对值是将负值替换为正值的函数)。这被称为平均绝对差L1 范数

1
2
3
dist_7_abs = (a_3 - mean7).abs().mean()
dist_7_sqr = ((a_3 - mean7)**2).mean().sqrt()
dist_7_abs,dist_7_sqr
1
(tensor(0.1586), tensor(0.3021))

等同于:

PyTorch 已经提供了这两种作为损失函数。您会在torch.nn.functional中找到这些,PyTorch 团队建议将其导入为F(并且默认情况下以这个名称在 fastai 中可用):

1
F.l1_loss(a_3.float(),mean7), F.mse_loss(a_3,mean7).sqrt()
1
(tensor(0.1586), tensor(0.3021))

在这里,MSE代表均方误差l1是标准数学术语平均绝对值的缩写(在数学中称为L1 范数)。

L1 范数和 均方误差(MSE)之间的区别

直观地,L1 范数和均方误差(MSE)之间的区别在于,后者会比前者更严厉地惩罚更大的错误(并对小错误更宽容)。

杰里米说

当我第一次遇到这个 L1 的东西时,我查了一下看它到底是什么意思。我在谷歌上发现它是使用“绝对值”作为“向量范数”,所以我查了“向量范数”并开始阅读:“给定一个实数或复数域 F 上的向量空间 V,V 上的范数是一个非负值的任意函数 p: V → [0,+∞),具有以下属性:对于所有的 a ∈ F 和所有的 u, v ∈ V,p(u + v) ≤ p(u) + p(v)…”然后我停止阅读。“唉,我永远也理解不了数学!”我想,这已经是第一千次了。从那时起,我学到了每当实践中出现这些复杂的数学术语时,我可以用一点点代码来替换它们!比如,L1 损失 只等于 (a-b).abs().mean(),其中 ab 是张量。我猜数学家们只是和我想法不同…我会确保在本书中,每当出现一些数学术语时,我会给你相应的代码片段,并用通俗的语言解释发生了什么。

我们刚刚在 PyTorch 张量上完成了各种数学运算。如果你之前在 PyTorch 中进行过数值编程,你可能会发现这些与 NumPy 数组相似。让我们来看看这两个重要的数据结构。

(请注意,fastai 在 NumPy 和 PyTorch 中添加了一些功能,使它们更加相似。如果本书中的任何代码在您的计算机上无法运行,可能是因为您忘记在笔记本的开头包含类似这样的一行代码:from fastai.vision.all import *。)

但是数组和张量是什么,为什么你应该关心呢?

Python 相对于许多语言来说速度较慢。在 Python、NumPy 或 PyTorch 中快速的任何东西,很可能是另一种语言(特别是 C)编写(并优化)的编译对象的包装器。事实上,NumPy 数组和 PyTorch 张量可以比纯 Python 快几千倍完成计算

NumPy 数组是一个多维数据表,所有项都是相同类型的。由于可以是任何类型,它们甚至可以是数组的数组,内部数组可能是不同大小的 - 这被称为 不规则数组。通过“多维数据表”,我们指的是,例如,一个列表(一维)、一个表或矩阵(二维)、一个表的表或立方体(三维),等等。如果所有项都是简单类型,如整数或浮点数,NumPy 将它们存储为紧凑的 C 数据结构在内存中。这就是 NumPy 的优势所在。NumPy 有各种运算符和方法,可以在这些紧凑结构上以优化的 C 速度运行计算,因为它们是用优化的 C 编写的。

PyTorch 张量几乎与 NumPy 数组相同,但有一个额外的限制,可以解锁额外的功能。它与 NumPy 数组相同,也是一个多维数据表,所有项都是相同类型的。然而,限制是张量不能使用任何旧类型 - 它必须对所有组件使用单一基本数值类型。因此,张量不像真正的数组数组那样灵活。例如,PyTorch 张量不能是不规则的。它始终是一个形状规则的多维矩形结构。

NumPy 在这些结构上支持的绝大多数方法和运算符在 PyTorch 上也支持,但 PyTorch 张量具有额外的功能。一个主要功能是这些结构可以存在于 GPU 上,这样它们的计算将被优化为 GPU,并且可以运行得更快(给定大量值进行处理)。此外,PyTorch 可以自动计算这些操作的导数,包括操作的组合。正如你将看到的,没有这种能力,实际上是不可能进行深度学习的。

如何有效地使用数组/张量 API 是最重要的新编码技能。

要创建一个数组或张量,将列表(或列表的列表,或列表的列表的列表等)传递给arraytensor

1
2
3
data = [[1,2,3],[4,5,6]]
arr = array (data)
tns = tensor(data)
1
arr  # numpy
1
2
array([[1, 2, 3],
[4, 5, 6]])
1
tns  # pytorch
1
2
tensor([[1, 2, 3],
[4, 5, 6]])

以下所有操作都是在张量上展示的,但 NumPy 数组的语法和结果是相同的。

你可以选择一行(请注意,与 Python 中的列表一样,张量是从 0 开始索引的,所以 1 指的是第二行/列):

1
tns[1]
1
tensor([4, 5, 6])

或者通过使用:来指示所有第一个轴(我们有时将张量/数组的维度称为)选择一列。

1
tns[:,1]
1
tensor([2, 5])

你可以结合 Python 切片语法([*start*:*end*],其中end被排除)来选择一行或一列的一部分:

1
tns[1,1:3]
1
tensor([5, 6])

你可以使用标准运算符,如+-*/

1
tns+1
1
2
tensor([[2, 3, 4],
[5, 6, 7]])

张量有一个类型:

1
tns.type()
1
'torch.LongTensor'

并且会根据需要自动更改该类型;例如,从intfloat

1
tns*1.5
1
2
tensor([[1.5000, 3.0000, 4.5000],
[6.0000, 7.5000, 9.0000]])

那么,我们的基准模型好吗?为了量化这一点,我们必须定义一个度量。

使用广播计算度量

回想一下度量是基于我们模型的预测和数据集中正确标签计算出来的一个数字,以告诉我们我们的模型有多好。例如,我们可以使用我们在上一节中看到的两个函数之一,均方误差或平均绝对误差,并计算整个数据集上它们的平均值。然而,这两个数字对大多数人来说并不是很容易理解;实际上,我们通常使用准确度作为分类模型的度量。

正如我们讨论过的,我们想要在验证集上计算我们的度量。这样我们就不会无意中过拟合——也就是说,训练一个模型只在我们的训练数据上表现良好。这对于我们在这里作为第一次尝试使用的像素相似度模型来说并不是真正的风险,因为它没有经过训练的组件,但我们仍然会使用一个验证集来遵循正常的实践,并为我们稍后的第二次尝试做好准备。

为了获得一个验证集,我们需要完全从训练数据中删除一些数据,这样模型根本就看不到它。事实证明,MNIST 数据集的创建者已经为我们做了这个。你还记得valid这个整个独立的目录吗?这个目录就是为此而设立的!

所以,让我们从那个目录中为我们的 3 和 7 创建张量。这些是我们将用来计算度量的张量,用来衡量我们第一次尝试模型的质量,这个度量衡量了与理想图像的距离:

1
2
3
4
5
6
7
valid_3_tens = torch.stack([tensor(Image.open(o))
for o in (path/'valid'/'3').ls()])
valid_3_tens = valid_3_tens.float()/255
valid_7_tens = torch.stack([tensor(Image.open(o))
for o in (path/'valid'/'7').ls()])
valid_7_tens = valid_7_tens.float()/255
valid_3_tens.shape,valid_7_tens.shape
1
(torch.Size([1010, 28, 28]), torch.Size([1028, 28, 28]))

在进行操作时检查形状是一个好习惯。在这里我们看到两个张量,一个代表了 1,010 张大小为 28×28 的 3 的验证集,另一个代表了 1,028 张大小为 28×28 的 7 的验证集。

我们最终想要编写一个函数is_3,它将决定任意图像是 3 还是 7。它将通过确定任意图像更接近我们的两个“理想数字”中的哪一个来实现这一点。为此,我们需要定义距离的概念——即,计算两个图像之间距离的函数。

我们可以编写一个简单的函数,使用与我们在上一节中编写的表达式非常相似的表达式来计算平均绝对误差:

1
2
def mnist_distance(a,b): return (a-b).abs().mean((-1,-2))
mnist_distance(a_3, mean3)
1
tensor(0.1114)

这是我们先前为这两个图像之间的距离计算的相同值,理想数字 3 mean_3和任意样本 3 a_3,它们都是形状为[28,28]的单个图像张量。

但是要计算整体准确度的指标,我们需要计算验证集中每张图像到理想数字 3 的距离。我们如何进行这种计算?我们可以编写一个循环,遍历验证集张量valid_3_tens中堆叠的所有单图像张量,其形状为[1010,28,28],表示 1,010 张图像。但是有一种更好的方法。

当我们使用相同的距离函数,设计用于比较两个单个图像,但将表示 3 的验证集张量valid_3_tens作为参数传入时,会发生一些有趣的事情:

1
2
valid_3_dist = mnist_distance(valid_3_tens, mean3)
valid_3_dist, valid_3_dist.shape
1
2
(tensor([0.1050, 0.1526, 0.1186,  ..., 0.1122, 0.1170, 0.1086]),
torch.Size([1010]))

它没有抱怨形状不匹配,而是为每个单个图像返回了一个距离(即,长度为 1,010 的秩-1 张量)。这是如何发生的?

再看看我们的函数mnist_distance,您会看到我们在那里有减法(a-b)。魔术技巧在于 PyTorch 在尝试在不同秩的两个张量之间执行简单的减法操作时,将使用广播:它将自动扩展秩较小的张量,使其大小与秩较大的张量相同。广播是一种重要的功能,使张量代码更容易编写。

在广播后,使两个参数张量具有相同的秩后,PyTorch 对于秩相同的两个张量应用其通常的逻辑:它对两个张量的每个对应元素执行操作,并返回张量结果。例如:

1
tensor([1,2,3]) + tensor([1,1,1])
1
tensor([2, 3, 4])

因此,在这种情况下,PyTorch 将mean3视为一个表示单个图像的秩-2 张量,就好像它是 1,010 个相同图像的副本,然后从我们的验证集中的每个 3 中减去每个副本。您期望这个张量的形状是什么?在查看这里的答案之前,请尝试自己想出来:

1
(valid_3_tens-mean3).shape
1
torch.Size([1010, 28, 28])

我们正在计算我们的理想数字 3 与验证集中的每个 1,010 个 3 之间的差异,对于每个 28×28 图像,结果形状为[1010,28,28]

有关广播实现的一些重要要点,使其不仅对于表达性有价值,而且对于性能也有价值:

  • PyTorch 实际上并没有将mean3复制 1,010 次。它假装它是一个具有该形状的张量,但不分配任何额外内存。

  • 它在 C 中完成整个计算(或者,如果您使用 GPU,则在 CUDA 中,相当于 GPU 上的 C),比纯 Python 快数万倍(在 GPU 上甚至快数百万倍!)。

这适用于 PyTorch 中所有广播和逐元素操作和函数。这是您要了解的最重要的技术,以创建高效的 PyTorch 代码。

接下来在mnist_distance中我们看到abs。现在您可能能猜到将其应用于张量时会发生什么。它将方法应用于张量中的每个单独元素,并返回结果的张量(即,它逐元素应用方法)。因此,在这种情况下,我们将得到 1,010 个绝对值。

最后,我们的函数调用mean((-1,-2))。元组(-1,-2)表示一系列轴。在 Python 中,-1指的是最后一个元素,-2指的是倒数第二个元素。因此,在这种情况下,这告诉 PyTorch 我们要对张量的最后两个轴的值进行平均。最后两个轴是图像的水平和垂直维度。在对最后两个轴进行平均后,我们只剩下第一个张量轴,它索引我们的图像,这就是为什么我们的最终大小是(1010)。换句话说,对于每个图像,我们对该图像中所有像素的强度进行了平均。

在本书中,我们将学习更多关于广播的知识,特别是在第十七章中,并且也会经常进行实践。

我们可以使用mnist_distance来确定一幅图像是否为 3,方法是使用以下逻辑:如果问题中的数字与理想的 3 之间的距离小于到理想的 7 的距离,则它是一个 3。这个函数将自动进行广播,并逐个应用,就像所有 PyTorch 函数和运算符一样:

1
def is_3(x): return mnist_distance(x,mean3) < mnist_distance(x,mean7)

让我们在我们的示例案例上测试一下:

1
is_3(a_3), is_3(a_3).float()
1
(tensor(True), tensor(1.))

请注意,当我们将布尔响应转换为浮点数时,True会得到1.0False会得到0.0

由于广播,我们还可以在所有 3 的完整验证集上进行测试:

1
is_3(valid_3_tens)
1
tensor([True, True, True,  ..., True, True, True])

现在我们可以计算每个 3 和 7 的准确率,方法是对所有 3 的函数取平均值,对所有 7 的函数取其倒数的平均值:

1
2
3
4
accuracy_3s =      is_3(valid_3_tens).float() .mean()
accuracy_7s = (1 - is_3(valid_7_tens).float()).mean()

accuracy_3s,accuracy_7s,(accuracy_3s+accuracy_7s)/2
1
(tensor(0.9168), tensor(0.9854), tensor(0.9511))

这看起来是一个相当不错的开始!我们在 3 和 7 上都获得了超过 90%的准确率,我们已经看到了如何使用广播方便地定义度量。但让我们诚实一点:3 和 7 是非常不同的数字。到目前为止,我们只对 10 个可能的数字中的 2 个进行分类。所以我们需要做得更好!

为了做得更好,也许现在是时候尝试一个真正学习的系统了,一个可以自动修改自身以提高性能的系统。换句话说,现在是时候谈论训练过程和 SGD 了。

随机梯度下降

你还记得 Arthur Samuel 在第一章中描述机器学习的方式吗?

假设我们安排一些自动手段来测试任何当前权重分配的有效性,以实际性能为基础,并提供一种机制来改变权重分配以最大化性能。我们不需要详细了解这种程序的细节,就可以看到它可以完全自动化,并且可以看到一个这样编程的机器会从中学习。

正如我们讨论过的,这是让我们拥有一个可以变得越来越好的模型的关键,可以学习。但我们的像素相似性方法实际上并没有做到这一点。我们没有任何权重分配,也没有任何根据测试权重分配的有效性来改进的方法。换句话说,我们无法通过修改一组参数来改进我们的像素相似性方法。为了充分利用深度学习的力量,我们首先必须按照 Samuel 描述的方式来表示我们的任务。

与其尝试找到图像与“理想图像”之间的相似性,我们可以查看每个单独的像素,并为每个像素提出一组权重,使得最高的权重与最有可能为特定类别的黑色像素相关联。例如,向右下方的像素不太可能被激活为 7,因此它们对于 7 的权重应该很低,但它们很可能被激活为 8,因此它们对于 8 的权重应该很高。这可以表示为一个函数和每个可能类别的一组权重值,例如,成为数字 8 的概率:

1
def pr_eight(x,w) = (x*w).sum()

在这里,我们假设X是图像,表示为一个向量—换句话说,所有行都堆叠在一起形成一个长长的单行。我们假设权重是一个向量W。如果我们有了这个函数,我们只需要一种方法来更新权重,使它们变得更好一点。通过这种方法,我们可以重复这个步骤多次,使权重变得越来越好,直到我们能够使它们尽可能好。

我们希望找到导致我们的函数对于那些是 8 的图像结果高,对于那些不是的图像结果低的向量W的特定值。搜索最佳向量W是搜索最佳函数以识别 8 的一种方式。(因为我们还没有使用深度神经网络,我们受到我们的函数能力的限制,我们将在本章后面解决这个约束。)

更具体地说,以下是将这个函数转化为机器学习分类器所需的步骤:

  1. 初始化权重。

  2. 对于每个图像,使用这些权重来预测它是 3 还是 7。

  3. 基于这些预测,计算模型有多好(它的损失)。

  4. 计算梯度,它衡量了每个权重的变化如何改变损失。

  5. 根据这个计算,改变(即,改变)所有权重。

  6. 回到步骤 2 并重复这个过程。

  7. 迭代直到你决定停止训练过程(例如,因为模型已经足够好或者你不想再等待了)。

    graph TD
    A[初始化权重] —> B[预测图像是3或7]
    B —> C[计算模型损失]
    C —> D[计算梯度]
    D —> E[更新权重]
    E —> F{停止条件满足?}
    F — 否 —> B
    F — 是 —> G[结束训练]

    style A fill:#f9f,stroke:#333
    style B fill:#bbf,stroke:#333
    style C fill:#bfb,stroke:#333
    style D fill:#ffb,stroke:#333
    style E fill:#fbb,stroke:#333
    style F fill:#fbf,stroke:#333,shape:hexagon
    style G fill:#9f9,stroke:#333</pre>

    这七个步骤,如图 4-1 所示,是所有深度学习模型训练的关键。深度学习完全依赖于这些步骤,这是非常令人惊讶和反直觉的。令人惊奇的是,这个过程可以解决如此复杂的问题。但是,正如你将看到的,它确实可以!

显示梯度下降步骤的图表

图 4-1. 梯度下降过程

每个步骤都有许多方法,我们将在本书的其余部分学习它们。这些细节对于深度学习从业者来说非常重要,但事实证明,对于每个步骤的一般方法都遵循一些基本原则。以下是一些建议:

初始化

我们将参数初始化为随机值。这可能听起来令人惊讶。我们当然可以做其他选择,比如将它们初始化为该类别激活该像素的百分比—但由于我们已经知道我们有一种方法来改进这些权重,结果证明只是从随机权重开始就可以完全正常运行。

损失

这就是 Samuel 所说的根据实际表现测试任何当前权重分配的有效性。我们需要一个函数,如果模型的表现好,它将返回一个小的数字(标准方法是将小的损失视为好的,大的损失视为坏的,尽管这只是一种约定)。

步骤

一个简单的方法来判断一个权重是否应该增加一点或减少一点就是尝试一下:增加一点权重,看看损失是增加还是减少。一旦找到正确的方向,你可以再多改变一点或少改变一点,直到找到一个效果好的量。然而,这很慢!正如我们将看到的,微积分的魔力使我们能够直接找出每个权重应该朝哪个方向改变,大概改变多少,而不必尝试所有这些小的改变。这样做的方法是通过计算梯度。这只是一种性能优化;我们也可以通过使用更慢的手动过程得到完全相同的结果。

停止

一旦我们决定要为模型训练多少个周期(之前的列表中给出了一些建议),我们就会应用这个决定。对于我们的数字分类器,我们会继续训练,直到模型的准确率开始变差,或者我们用完时间为止。

在将这些步骤应用于我们的图像分类问题之前,让我们在一个更简单的情况下看看它们是什么样子。首先我们将定义一个非常简单的函数,二次函数—假设这是我们的损失函数,x是函数的权重参数:

1
def f(x): return x**2

这是该函数的图表:

1
plot_function(f, 'x', 'x**2')

我们之前描述的步骤序列从选择参数的随机值开始,并计算损失的值:

1
2
plot_function(f, 'x', 'x**2')
plt.scatter(-1.5, f(-1.5), color='red');

现在我们来看看如果我们稍微增加或减少参数会发生什么—调整。这只是特定点的斜率:

显示在某一点的斜率的平方函数的图表

我们可以稍微改变我们的权重朝着斜坡的方向,计算我们的损失和调整,然后再重复几次。最终,我们将到达曲线上的最低点:

梯度下降的示意图

这个基本思想最早可以追溯到艾萨克·牛顿,他指出我们可以以这种方式优化任意函数。无论我们的函数变得多么复杂,梯度下降的这种基本方法不会有太大变化。我们在本书后面看到的唯一微小变化是一些方便的方法,可以让我们更快地找到更好的步骤。

计算梯度

唯一的魔法步骤是计算梯度的部分。正如我们提到的,我们使用微积分作为性能优化;它让我们更快地计算当我们调整参数时我们的损失会上升还是下降。换句话说,梯度将告诉我们我们需要改变每个权重多少才能使我们的模型更好。

您可能还记得高中微积分课上的导数告诉您函数参数的变化会如何改变其结果。如果不记得,不用担心;我们很多人高中毕业后就忘了微积分!但在继续之前,您需要对导数有一些直观的理解,所以如果您对此一头雾水,可以前往 Khan Academy 完成基本导数课程。您不必自己计算导数;您只需要知道导数是什么。

导数的关键点在于:对于任何函数,比如我们在前一节中看到的二次函数,我们可以计算它的导数。导数是另一个函数。它计算的是变化,而不是值。例如,在值为 3 时,二次函数的导数告诉我们函数在值为 3 时的变化速度。更具体地说,您可能还记得梯度被定义为上升/水平移动;也就是说,函数值的变化除以参数值的变化。当我们知道我们的函数将如何变化时,我们就知道我们需要做什么来使它变小。这是机器学习的关键:有一种方法来改变函数的参数使其变小。微积分为我们提供了一个计算的捷径,即导数,它让我们直接计算我们函数的梯度。

一个重要的事情要注意的是我们的函数有很多需要调整的权重,所以当我们计算导数时,我们不会得到一个数字,而是很多个—每个权重都有一个梯度。但在这里没有数学上的技巧;您可以计算相对于一个权重的导数,将其他所有权重视为常数,然后对每个其他权重重复这个过程。这就是计算所有梯度的方法,对于每个权重。

刚才我们提到您不必自己计算任何梯度。这怎么可能?令人惊讶的是,PyTorch 能够自动计算几乎任何函数的导数!而且,它计算得非常快。大多数情况下,它至少与您手动创建的任何导数函数一样快。让我们看一个例子。

首先,让我们选择一个张量数值,我们想要梯度:

1
xt = tensor(3.).requires_grad_()

注意特殊方法requires_grad_?这是我们告诉 PyTorch 我们想要计算梯度的神奇咒语。这实质上是给变量打上标记,这样 PyTorch 就会记住如何计算您要求的其他直接计算的梯度。

好像仅仅是求导

Alexis 说

如果您来自数学或物理学,这个 API 可能会让您困惑。在这些背景下,函数的“梯度”只是另一个函数(即,它的导数),因此您可能期望与梯度相关的 API 提供给您一个新函数。但在深度学习中,“梯度”通常意味着函数的导数在特定参数值处的。PyTorch API 也将重点放在参数上,而不是您实际计算梯度的函数。起初可能感觉有些反常,但这只是一个不同的视角。

现在我们用这个值计算我们的函数。注意 PyTorch 打印的不仅是计算的值,还有一个提示,它有一个梯度函数将在需要时用来计算我们的梯度:

1
2
yt = f(xt)
yt
1
tensor(9., grad_fn=<PowBackward0>)

最后,我们告诉 PyTorch 为我们计算梯度:

1
yt.backward()

这里的backward指的是反向传播,这是计算每一层导数的过程的名称。我们将在第十七章中看到这是如何精确完成的,当我们从头开始计算深度神经网络的梯度时。这被称为网络的反向传播,与前向传播相对,前者是计算激活的地方。如果backward只是被称为calculate_grad,生活可能会更容易,但深度学习的人确实喜欢在任何地方添加行话!

我们现在可以通过检查我们张量的grad属性来查看梯度:

1
xt.grad
1
tensor(6.)

如果您记得高中微积分规则,x**2的导数是2*x,我们有x=3,所以梯度应该是2*3=6,这就是 PyTorch 为我们计算的结果!

现在我们将重复前面的步骤,但使用一个向量参数来计算我们的函数:

1
2
xt = tensor([3.,4.,10.]).requires_grad_()
xt
1
tensor([ 3.,  4., 10.], requires_grad=True)

并且我们将sum添加到我们的函数中,以便它可以接受一个向量(即,一个秩为 1 的张量)并返回一个标量(即,一个秩为 0 的张量):

1
2
3
4
def f(x): return (x**2).sum()

yt = f(xt)
yt
1
tensor(125., grad_fn=<SumBackward0>)

我们的梯度是2*xt,正如我们所期望的!

1
2
yt.backward()
xt.grad
1
tensor([ 6.,  8., 20.])

梯度告诉我们函数的斜率;它们并不告诉我们要调整参数多远。但它们确实给了我们一些想法:如果斜率非常大,那可能意味着我们需要更多的调整,而如果斜率非常小,那可能意味着我们接近最优值。

使用学习率进行步进

根据梯度值来决定如何改变我们的参数是深度学习过程中的一个重要部分。几乎所有方法都从一个基本思想开始,即将梯度乘以一些小数字,称为学习率(LR)。学习率通常是 0.001 到 0.1 之间的数字,尽管它可以是任何值。通常人们通过尝试几个学习率来选择一个,并找出哪个在训练后产生最佳模型的结果(我们将在本书后面展示一个更好的方法,称为学习率查找器)。一旦选择了学习率,您可以使用这个简单函数调整参数:

1
w -= w.grad * lr

这被称为调整您的参数,使用优化步骤

如果您选择的学习率太低,可能意味着需要执行很多步骤。图 4-2 说明了这一点。

梯度下降示例,学习率过低

图 4-2。学习率过低的梯度下降

但选择一个学习率太高的学习率更糟糕——它可能导致损失变得更糟,正如我们在图 4-3 中看到的!

学习率过高的梯度下降示例

图 4-3. 学习率过高的梯度下降

如果学习率太高,它也可能会“弹跳”而不是发散;图 4-4 显示了这样做需要许多步骤才能成功训练。

带有弹跳学习率的梯度下降示例

图 4-4. 带有弹跳学习率的梯度下降

现在让我们在一个端到端的示例中应用所有这些。

一个端到端的 SGD 示例

我们已经看到如何使用梯度来最小化我们的损失。现在是时候看一个 SGD 示例,并看看如何找到最小值来训练模型以更好地拟合数据。

让我们从一个简单的合成示例模型开始。想象一下,您正在测量过山车通过顶峰时的速度。它会开始快速,然后随着上坡而变慢;在顶部最慢,然后在下坡时再次加速。您想建立一个关于速度随时间变化的模型。如果您每秒手动测量速度 20 秒,它可能看起来像这样:

1
time = torch.arange(0,20).float(); time
1
2
tensor([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10., 11., 12., 13.,
> 14., 15., 16., 17., 18., 19.])
1
2
speed = torch.randn(20)*3 + 0.75*(time-9.5)**2 + 1
plt.scatter(time,speed);

我们添加了一些随机噪声,因为手动测量不够精确。这意味着很难回答问题:过山车的速度是多少?使用 SGD,我们可以尝试找到一个与我们的观察相匹配的函数。我们无法考虑每种可能的函数,所以让我们猜测它将是二次的;即,一个形式为a*(time**2)+(b*time)+c的函数。

我们希望清楚地区分函数的输入(我们测量过山车速度的时间)和其参数(定义我们正在尝试的二次函数的值)。因此,让我们将参数收集在一个参数中,从而在函数的签名中分离输入t和参数params

1
2
3
def f(t, params):
a,b,c = params
return a*(t**2) + (b*t) + c

换句话说,我们已经将找到最佳拟合数据的最佳函数的问题限制为找到最佳二次函数。这极大地简化了问题,因为每个二次函数都由三个参数abc完全定义。因此,要找到最佳二次函数,我们只需要找到最佳的abc的值。

如果我们可以解决二次函数的三个参数的问题,我们就能够对其他具有更多参数的更复杂函数应用相同的方法——比如神经网络。让我们先找到f的参数,然后我们将回来对 MNIST 数据集使用神经网络做同样的事情。

首先,我们需要定义“最佳”是什么意思。我们通过选择一个损失函数来精确定义这一点,该函数将根据预测和目标返回一个值,其中函数的较低值对应于“更好”的预测。对于连续数据,通常使用均方误差

1
def mse(preds, targets): return ((preds-targets)**2).mean()

现在,让我们按照我们的七步流程进行工作。

第一步:初始化参数

首先,我们将参数初始化为随机值,并告诉 PyTorch 我们要使用requires_grad_跟踪它们的梯度:

1
params = torch.randn(3).requires_grad_()

第二步:计算预测

接下来,我们计算预测:

1
preds = f(time, params)

让我们创建一个小函数来查看我们的预测与目标的接近程度,并看一看:

1
2
3
4
5
def show_preds(preds, ax=None):
if ax is None: ax=plt.subplots()[1]
ax.scatter(time, speed)
ax.scatter(time, to_np(preds), color='red')
ax.set_ylim(-300,100)
1
show_preds(preds)

这看起来并不接近——我们的随机参数表明过山车最终会倒退,因为我们有负速度!

第一步:初始化参数

首先,我们将参数初始化为随机值,并告诉 PyTorch 我们要使用requires_grad_跟踪它们的梯度:

1
params = torch.randn(3).requires_grad_()

第二步:计算预测

接下来,我们计算预测:

1
preds = f(time, params)

让我们创建一个小函数来查看我们的预测与目标的接近程度,并看一看:

1
2
3
4
5
def show_preds(preds, ax=None):
if ax is None: ax=plt.subplots()[1]
ax.scatter(time, speed)
ax.scatter(time, to_np(preds), color='red')
ax.set_ylim(-300,100)
1
show_preds(preds)

这看起来并不接近——我们的随机参数表明过山车最终会倒退,因为我们有负速度!

第三步:计算损失

我们计算损失如下:

1
2
loss = mse(preds, speed)
loss
1
tensor(25823.8086, grad_fn=<MeanBackward0>)

我们的目标现在是改进这一点。为了做到这一点,我们需要知道梯度。

第四步:计算梯度

下一步是计算梯度,或者近似参数需要如何改变:

1
2
loss.backward()
params.grad
1
tensor([-53195.8594,  -3419.7146,   -253.8908])
1
params.grad * 1e-5
1
tensor([-0.5320, -0.0342, -0.0025])

我们可以利用这些梯度来改进我们的参数。我们需要选择一个学习率(我们将在下一章中讨论如何在实践中做到这一点;现在,我们将使用 1e-5 或 0.00001):

1
params
1
tensor([-0.7658, -0.7506,  1.3525], requires_grad=True)

第 5 步:调整权重

现在我们需要根据刚刚计算的梯度更新参数:

1
2
3
lr = 1e-5
params.data -= lr * params.grad.data
params.grad = None

Alexis 说

理解这一点取决于记住最近的历史。为了计算梯度,我们在loss上调用backward。但是这个loss本身是通过mse计算的,而mse又以preds作为输入,preds是使用f计算的,fparams作为输入,params是我们最初调用required_grads_的对象,这是最初的调用,现在允许我们在loss上调用backward。这一系列函数调用代表了函数的数学组合,使得 PyTorch 能够在幕后使用微积分的链式法则来计算这些梯度。

让我们看看损失是否有所改善:

1
2
preds = f(time,params)
mse(preds, speed)
1
tensor(5435.5366, grad_fn=<MeanBackward0>)

再看一下图表:

1
show_preds(preds)

我们需要重复这个过程几次,所以我们将创建一个应用一步的函数:

1
2
3
4
5
6
7
8
def apply_step(params, prn=True):
preds = f(time, params)
loss = mse(preds, speed)
loss.backward()
params.data -= lr * params.grad.data
params.grad = None
if prn: print(loss.item())
return preds

第 6 步:重复这个过程

现在我们进行迭代。通过循环和进行许多改进,我们希望达到一个好的结果:

1
for i in range(10): apply_step(params)
1
2
3
4
5
6
7
8
9
10
5435.53662109375
1577.4495849609375
847.3780517578125
709.22265625
683.0757446289062
678.12451171875
677.1839599609375
677.0025024414062
676.96435546875
676.9537353515625

损失正在下降,正如我们所希望的!但仅仅看这些损失数字掩盖了一个事实,即每次迭代代表尝试一个完全不同的二次函数,以找到最佳可能的二次函数。如果我们不打印出损失函数,而是在每一步绘制函数,我们可以看到形状是如何接近我们的数据的最佳可能的二次函数:

1
2
3
_,axs = plt.subplots(1,4,figsize=(12,3))
for ax in axs: show_preds(apply_step(params, False), ax)
plt.tight_layout()

第 7 步:停止

我们刚刚决定在任意选择的 10 个 epochs 后停止。在实践中,我们会观察训练和验证损失以及我们的指标,以决定何时停止,正如我们所讨论的那样。

总结梯度下降

现在您已经看到每个步骤中发生的事情,让我们再次看一下我们的梯度下降过程的图形表示(图 4-5)并进行一个快速回顾。

显示梯度下降步骤的图表

图 4-5. 梯度下降过程

在开始时,我们模型的权重可以是随机的(从头开始训练)或来自预训练模型(迁移学习)。在第一种情况下,我们从输入得到的输出与我们想要的完全无关,即使在第二种情况下,预训练模型也可能不太擅长我们所针对的特定任务。因此,模型需要学习更好的权重。

我们首先将模型给出的输出与我们的目标进行比较(我们有标记数据,所以我们知道模型应该给出什么结果),使用一个损失函数,它返回一个数字,我们希望通过改进我们的权重使其尽可能低。为了做到这一点,我们从训练集中取出一些数据项(如图像)并将它们馈送给我们的模型。我们使用我们的损失函数比较相应的目标,我们得到的分数告诉我们我们的预测有多么错误。然后我们稍微改变权重使其稍微更好。

为了找出如何改变权重使损失稍微变好,我们使用微积分来计算梯度。(实际上,我们让 PyTorch 为我们做这个!)让我们考虑一个类比。想象一下你在山上迷路了,你的车停在最低点。为了找到回去的路,你可能会朝着随机方向走,但那可能不会有太大帮助。由于你知道你的车在最低点,你最好是往下走。通过始终朝着最陡峭的下坡方向迈出一步,你最终应该到达目的地。我们使用梯度的大小(即坡度的陡峭程度)来告诉我们应该迈多大一步;具体来说,我们将梯度乘以我们选择的一个称为学习率的数字来决定步长。然后我们迭代直到达到最低点,那将是我们的停车场;然后我们可以停止

我们刚刚看到的所有内容都可以直接转换到 MNIST 数据集,除了损失函数。现在让我们看看如何定义一个好的训练目标。

MNIST 损失函数

我们已经有了我们的x—也就是我们的自变量,图像本身。我们将它们全部连接成一个单一的张量,并且还将它们从矩阵列表(一个秩为 3 的张量)转换为向量列表(一个秩为 2 的张量)。我们可以使用view来做到这一点,view是一个 PyTorch 方法,可以改变张量的形状而不改变其内容。-1view的一个特殊参数,意思是“使这个轴尽可能大以适应所有数据”:

1
train_x = torch.cat([stacked_threes, stacked_sevens]).view(-1, 28*28)

我们需要为每张图片标记。我们将使用1表示 3,0表示 7:

1
2
train_y = tensor([1]*len(threes) + [0]*len(sevens)).unsqueeze(1)
train_x.shape,train_y.shape
1
(torch.Size([12396, 784]), torch.Size([12396, 1]))

在 PyTorch 中,当索引时,Dataset需要返回一个(x,y)元组。Python 提供了一个zip函数,当与list结合使用时,可以简单地实现这个功能:

1
2
3
dset = list(zip(train_x,train_y))
x,y = dset[0]
x.shape,y
1
(torch.Size([784]), tensor([1]))
1
2
3
valid_x = torch.cat([valid_3_tens, valid_7_tens]).view(-1, 28*28)
valid_y = tensor([1]*len(valid_3_tens) + [0]*len(valid_7_tens)).unsqueeze(1)
valid_dset = list(zip(valid_x,valid_y))

现在我们需要为每个像素(最初是随机的)分配一个权重(这是我们七步过程中的初始化步骤):

1
def init_params(size, std=1.0): return (torch.randn(size)*std).requires_grad_()
1
weights = init_params((28*28,1))

函数weights*pixels不够灵活—当像素等于 0 时,它总是等于 0(即其截距为 0)。你可能还记得高中数学中线的公式是y=w*x+b;我们仍然需要b。我们也会将其初始化为一个随机数:

1
bias = init_params(1)

在神经网络中,方程y=w*x+b中的w被称为权重b被称为偏置。权重和偏置一起构成参数

术语:参数

模型的权重偏置。权重是方程w*x+b中的w,偏置是该方程中的b

现在我们可以为一张图片计算一个预测:

1
(train_x[0]*weights.T).sum() + bias
1
tensor([20.2336], grad_fn=<AddBackward0>)

虽然我们可以使用 Python 的for循环来计算每张图片的预测,但那将非常慢。因为 Python 循环不在 GPU 上运行,而且因为 Python 在一般情况下循环速度较慢,我们需要尽可能多地使用高级函数来表示模型中的计算。

在这种情况下,有一个非常方便的数学运算可以为矩阵的每一行计算w*x—它被称为矩阵乘法。图 4-6 展示了矩阵乘法的样子。

矩阵乘法

图 4-6. 矩阵乘法

这幅图展示了两个矩阵AB相乘。结果的每个项目,我们称之为AB,包含了A的对应行的每个项目与B的对应列的每个项目相乘后相加。例如,第 1 行第 2 列(带有红色边框的黄色点)计算为。如果您需要复习矩阵乘法,我们建议您查看 Khan Academy 的“矩阵乘法简介”,因为这是深度学习中最重要的数学运算。

在 Python 中,矩阵乘法用@运算符表示。让我们试一试:

1
2
3
def linear1(xb): return xb@weights + bias
preds = linear1(train_x)
preds
1
2
3
4
5
6
7
tensor([[20.2336],
[17.0644],
[15.2384],
...,
[18.3804],
[23.8567],
[28.6816]], grad_fn=<AddBackward0>)

第一个元素与我们之前计算的相同,正如我们所期望的。这个方程batch @ weights + bias是任何神经网络的两个基本方程之一(另一个是激活函数,我们马上会看到)。

让我们检查我们的准确性。为了确定输出代表 3 还是 7,我们只需检查它是否大于 0,因此我们可以计算每个项目的准确性(使用广播,因此没有循环!)如下:

1
2
corrects = (preds>0.0).float() == train_y
corrects
1
2
3
4
5
6
7
tensor([[ True],
[ True],
[ True],
...,
[False],
[False],
[False]])
1
corrects.float().mean().item()
1
0.4912068545818329

现在让我们看看一个权重的微小变化对准确性的影响是什么:

1
weights[0] *= 1.0001
1
2
preds = linear1(train_x)
((preds>0.0).float() == train_y).float().mean().item()
1
0.4912068545818329

正如我们所看到的,我们需要梯度来通过 SGD 改进我们的模型,为了计算梯度,我们需要一个损失函数,它代表了我们的模型有多好。这是因为梯度是损失函数如何随着对权重的微小调整而变化的度量。

因此,我们需要选择一个损失函数。显而易见的方法是使用准确性作为我们的度量标准,也作为我们的损失函数。在这种情况下,我们将为每个图像计算我们的预测,收集这些值以计算总体准确性,然后计算每个权重相对于总体准确性的梯度。

不幸的是,我们在这里有一个重要的技术问题。函数的梯度是其斜率,或者是其陡峭程度,可以定义为上升与下降——也就是说,函数值上升或下降的幅度,除以我们改变输入的幅度。我们可以用数学方式写成:

1
(y_new – y_old) / (x_new – x_old)

x_new非常类似于x_old时,这给出了梯度的良好近似,这意味着它们的差异非常小。但是,只有当预测从 3 变为 7,或者反之时,准确性才会发生变化。问题在于,从x_oldx_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。这意味着我们的损失函数将接收这些值作为其输入:

1
2
trgts  = tensor([1,0,1])
prds = tensor([0.9, 0.4, 0.2])

这是一个测量predictionstargets之间距离的损失函数的第一次尝试:

1
2
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 文档站点上查找。

让我们在我们的prdstrgts上尝试一下:

1
torch.where(trgts==1, 1-prds, prds)
1
tensor([0.1000, 0.4000, 0.8000])

您可以看到,当预测更准确时,当准确预测更自信时(绝对值更高),以及当不准确预测更不自信时,此函数返回较低的数字。在 PyTorch 中,我们始终假设损失函数的较低值更好。由于我们需要一个标量作为最终损失,mnist_loss取前一个张量的平均值:

1
mnist_loss(prds,trgts)
1
tensor(0.4333)

例如,如果我们将对一个“错误”目标的预测从0.2更改为0.8,损失将减少,表明这是一个更好的预测:

1
mnist_loss(tensor([0.9, 0.4, 0.8]),trgts)
1
tensor(0.2333)

mnist_loss当前定义的一个问题是它假设预测总是在 0 和 1 之间。因此,我们需要确保这实际上是这种情况!恰好有一个函数可以做到这一点,让我们来看看。

Sigmoid

sigmoid函数总是输出一个介于 0 和 1 之间的数字。它的定义如下:

1
def sigmoid(x): return 1/(1+torch.exp(-x))

PyTorch 为我们定义了一个加速版本,所以我们不需要自己的。这是深度学习中一个重要的函数,因为我们经常希望确保数值在 0 和 1 之间。它看起来是这样的:

1
plot_function(torch.sigmoid, title='Sigmoid', min=-4, max=4)

正如您所看到的,它接受任何输入值,正数或负数,并将其压缩为 0 和 1 之间的输出值。它还是一个只上升的平滑曲线,这使得 SGD 更容易找到有意义的梯度。

让我们更新mnist_loss,首先对输入应用sigmoid

1
2
3
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 集合转换为一个迭代器,用于生成多个批次,就像这样:

1
2
3
coll = range(15)
dl = DataLoader(coll, batch_size=5, shuffle=True)
list(dl)
1
2
3
[tensor([ 3, 12,  8, 10,  2]),
tensor([ 9, 4, 7, 14, 5]),
tensor([ 1, 13, 0, 6, 11])]

对于训练模型,我们不只是想要任何 Python 集合,而是一个包含独立和相关变量(模型的输入和目标)的集合。包含独立和相关变量元组的集合在 PyTorch 中被称为Dataset。这是一个极其简单的Dataset的示例:

1
2
ds = L(enumerate(string.ascii_lowercase))
ds
1
2
(#26) [(0, 'a'),(1, 'b'),(2, 'c'),(3, 'd'),(4, 'e'),(5, 'f'),(6, 'g'),(7,
> 'h'),(8, 'i'),(9, 'j')...]

当我们将Dataset传递给DataLoader时,我们将得到许多批次,它们本身是表示独立和相关变量批次的张量元组:

1
2
dl = DataLoader(ds, batch_size=6, shuffle=True)
list(dl)
1
2
3
4
5
[(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 中看到的过程了。在代码中,我们的过程将为每个时期实现类似于这样的东西:

1
2
3
4
5
for x,y in dl:
pred = model(x)
loss = loss_func(pred, y)
loss.backward()
parameters -= parameters.grad * lr

首先,让我们重新初始化我们的参数:

1
2
weights = init_params((28*28,1))
bias = init_params(1)

DataLoader可以从Dataset创建:

1
2
3
dl = DataLoader(dset, batch_size=256)
xb,yb = first(dl)
xb.shape,yb.shape
1
(torch.Size([256, 784]), torch.Size([256, 1]))

我们将对验证集执行相同的操作:

1
valid_dl = DataLoader(valid_dset, batch_size=256)

让我们创建一个大小为 4 的小批量进行测试:

1
2
batch = train_x[:4]
batch.shape
1
torch.Size([4, 784])
1
2
preds = linear1(batch)
preds
1
2
3
4
tensor([[-11.1002],
[ 5.9263],
[ 9.9627],
[ -8.1484]], grad_fn=<AddBackward0>)
1
2
loss = mnist_loss(preds, train_y[:4])
loss
1
tensor(0.5006, grad_fn=<MeanBackward0>)

现在我们可以计算梯度了:

1
2
loss.backward()
weights.grad.shape,weights.grad.mean(),bias.grad
1
(torch.Size([784, 1]), tensor(-0.0001), tensor([-0.0008]))

让我们把所有这些放在一个函数中:

1
2
3
4
def calc_grad(xb, yb, model):
preds = model(xb)
loss = mnist_loss(preds, yb)
loss.backward()

并测试它:

1
2
calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(),bias.grad
1
(tensor(-0.0002), tensor([-0.0015]))

但是看看如果我们调用两次会发生什么:

1
2
calc_grad(batch, train_y[:4], linear1)
weights.grad.mean(),bias.grad
1
(tensor(-0.0003), tensor([-0.0023]))

梯度已经改变了!这是因为loss.backward 添加loss的梯度到当前存储的任何梯度中。因此,我们首先必须将当前梯度设置为 0:

1
2
weights.grad.zero_()
bias.grad.zero_();

原地操作

PyTorch 中以下划线结尾的方法会原地修改它们的对象。例如,bias.zero_会将张量bias的所有元素设置为 0。

我们唯一剩下的步骤是根据梯度和学习率更新权重和偏差。当我们这样做时,我们必须告诉 PyTorch 不要对这一步骤进行梯度计算,否则当我们尝试在下一个批次计算导数时会变得混乱!如果我们将张量的data属性赋值,PyTorch 将不会对该步骤进行梯度计算。这是我们用于一个时期的基本训练循环:

1
2
3
4
5
6
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。因此,我们可以计算每个项目的准确性(使用广播,所以没有循环!)如下:

1
(preds>0.0).float() == train_y[:4]
1
2
3
4
tensor([[False],
[ True],
[ True],
[False]])

这给了我们计算验证准确性的这个函数:

1
2
3
4
def batch_accuracy(xb, yb):
preds = xb.sigmoid()
correct = (preds>0.5) == yb
return correct.float().mean()

我们可以检查它是否有效:

1
batch_accuracy(linear1(batch), train_y[:4])
1
tensor(0.5000)

然后把批次放在一起:

1
2
3
def validate_epoch(model):
accs = [batch_accuracy(model(xb), yb) for xb,yb in valid_dl]
return round(torch.stack(accs).mean().item(), 4)
1
validate_epoch(linear1)
1
0.5219

这是我们的起点。让我们训练一个时期,看看准确性是否提高:

1
2
3
4
lr = 1.
params = weights,bias
train_epoch(linear1, lr, params)
validate_epoch(linear1)
1
0.6883

然后再做几次:

1
2
3
for i in range(20):
train_epoch(linear1, lr, params)
print(validate_epoch(linear1), end=' ')
1
2
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_paramslinear一样。它包含了权重偏差在一个单独的类中。这是我们如何复制上一节中的模型:

1
linear_model = nn.Linear(28*28,1)

每个 PyTorch 模块都知道它有哪些可以训练的参数;它们可以通过parameters方法获得:

1
2
w,b = linear_model.parameters()
w.shape,b.shape
1
(torch.Size([1, 784]), torch.Size([1]))

我们可以使用这些信息创建一个优化器:

1
2
3
4
5
6
7
8
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

我们可以通过传入模型的参数来创建优化器:

1
opt = BasicOptim(linear_model.parameters(), lr)

我们的训练循环现在可以简化:

1
2
3
4
5
def train_epoch(model):
for xb,yb in dl:
calc_grad(xb, yb, model)
opt.step()
opt.zero_grad()

我们的验证函数不需要任何更改:

1
validate_epoch(linear_model)
1
0.4157

让我们把我们的小训练循环放在一个函数中,让事情变得更简单:

1
2
3
4
def train_model(model, epochs):
for i in range(epochs):
train_epoch(model)
print(validate_epoch(model), end=' ')

结果与上一节相同:

1
train_model(linear_model, 20)
1
2
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做相同的事情:

1
2
3
linear_model = nn.Linear(28*28,1)
opt = SGD(linear_model.parameters(), lr)
train_model(linear_model, 20)
1
2
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

1
dls = DataLoaders(dl, valid_dl)

要创建一个Learner而不使用应用程序(如cnn_learner),我们需要传入本章中创建的所有元素:DataLoaders,模型,优化函数(将传递参数),损失函数,以及可选的任何要打印的指标:

1
2
learn = Learner(dls, nn.Linear(28*28,1), opt_func=SGD,
loss_func=mnist_loss, metrics=batch_accuracy)

现在我们可以调用fit

1
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 不同的东西)——这就是给我们神经网络的东西。

这是一个基本神经网络的完整定义:

1
2
3
4
5
def simple_net(xb):
res = xb@w1 + b1
res = res.max(tensor(0.0))
res = res@w2 + b2
return res

就是这样!在simple_net中,我们只有两个线性分类器,它们之间有一个max函数。

在这里,w1w2是权重张量,b1b2是偏置张量;也就是说,这些参数最初是随机初始化的,就像我们在上一节中所做的一样:

1
2
3
4
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使用:

1
plot_function(F.relu)

Jeremy 说

深度学习中有大量行话,包括修正线性单元等术语。绝大多数这些行话并不比我们在这个例子中看到的一行代码更复杂。事实是,学术界为了发表论文,他们需要让论文听起来尽可能令人印象深刻和复杂。他们通过引入行话来实现这一点。不幸的是,这导致该领域变得比应该更加令人生畏和难以进入。您确实需要学习这些行话,因为否则论文和教程对您来说将毫无意义。但这并不意味着您必须觉得这些行话令人生畏。只需记住,当您遇到以前未见过的单词或短语时,它几乎肯定是指一个非常简单的概念。

基本思想是通过使用更多的线性层,我们的模型可以进行更多的计算,从而模拟更复杂的函数。但是,直接将一个线性布局放在另一个线性布局之后是没有意义的,因为当我们将事物相乘然后多次相加时,可以用不同的事物相乘然后只相加一次来替代!也就是说,一系列任意数量的线性层可以被替换为具有不同参数集的单个线性层。

但是,如果我们在它们之间放置一个非线性函数,比如max,这就不再成立了。现在每个线性层都有点解耦,可以做自己有用的工作。max函数特别有趣,因为它作为一个简单的if语句运行。

Sylvain 说

数学上,我们说两个线性函数的组合是另一个线性函数。因此,我们可以堆叠任意多个线性分类器在一起,而它们之间没有非线性函数,这将与一个线性分类器相同。

令人惊讶的是,可以数学证明这个小函数可以解决任何可计算问题,只要你能找到w1w2的正确参数,并且使这些矩阵足够大。对于任何任意波动的函数,我们可以将其近似为一堆连接在一起的线条;为了使其更接近波动函数,我们只需使用更短的线条。这被称为通用逼近定理。我们这里的三行代码被称为。第一和第三行被称为线性层,第二行代码被称为非线性激活函数

就像在前一节中一样,我们可以利用 PyTorch 简化这段代码:

1
2
3
4
5
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是一个模块,我们可以获取它的参数,它将返回它包含的所有模块的所有参数的列表。让我们试一试!由于这是一个更深层的模型,我们将使用更低的学习率和更多的周期:

1
2
learn = Learner(dls, simple_net, opt_func=SGD,
loss_func=mnist_loss, metrics=batch_accuracy)
1
learn.fit(40, 0.1)

我们这里不展示 40 行输出,以节省空间;训练过程记录在learn.recorder中,输出表存储在values属性中,因此我们可以绘制训练过程中的准确性:

1
plt.plot(L(learn.recorder.values).itemgot(2));

我们可以查看最终的准确性:

1
learn.recorder.values[-1][2]
1
0.982826292514801

在这一点上,我们有一些非常神奇的东西:

  • 给定正确的参数集,可以解决任何问题到任何精度的函数(神经网络)

  • 找到任何函数的最佳参数集的方法(随机梯度下降)

这就是为什么深度学习可以做出如此奇妙的事情。相信这些简单技术的组合确实可以解决任何问题是我们发现许多学生必须迈出的最大步骤之一。这似乎太好了,以至于难以置信——事情肯定应该比这更困难和复杂吧?我们的建议是:试一试!我们刚刚在 MNIST 数据集上尝试了一下,你已经看到了结果。由于我们自己从头开始做所有事情(除了计算梯度),所以你知道背后没有隐藏任何特殊的魔法。

更深入地探讨

我们不必止步于只有两个线性层。我们可以添加任意数量的线性层,只要在每对线性层之间添加一个非线性。然而,正如您将了解的那样,模型变得越深,实际中优化参数就越困难。在本书的后面,您将学习一些简单但非常有效的训练更深层模型的技巧。

我们已经知道,一个带有两个线性层的单个非线性足以逼近任何函数。那么为什么要使用更深的模型呢?原因是性能。通过更深的模型(具有更多层),我们不需要使用太多参数;事实证明,我们可以使用更小的矩阵,更多的层,获得比使用更大的矩阵和少量层获得更好的结果。

这意味着我们可以更快地训练模型,并且它将占用更少的内存。在 1990 年代,研究人员如此专注于通用逼近定理,以至于很少有人尝试超过一个非线性。这种理论但不实际的基础阻碍了该领域多年。然而,一些研究人员确实尝试了深度模型,并最终能够证明这些模型在实践中表现得更好。最终,出现了理论结果,解释了为什么会发生这种情况。今天,几乎不可能找到任何人只使用一个非线性的神经网络。

当我们使用与我们在第一章中看到的相同方法训练一个 18 层模型时会发生什么:

1
2
3
4
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 和第三章节了吗?好吧,这里提醒你现在回到第二章,因为你很快就会需要了解那些内容!

课后习题

  1. 灰度图像在计算机上是如何表示的?彩色图像呢?

    • 灰度图像通过单通道的二维矩阵(每个像素为0-255的亮度值)表示,而彩色图像通过三通道(RGB)的三维数组(每个像素包含红、绿、蓝三个0-255的强度值)表示。
  2. MNIST_SAMPLE数据集中的文件和文件夹是如何结构化的?为什么?

    • MNIST_SAMPLE数据集通常按类别分层组织(如train/3、train/7和valid/3、valid/7),这种结构便于机器学习框架(如PyTorch的ImageFolder)自动识别标签并划分训练集/验证集,简化数据加载流程。
  3. 解释“像素相似性”方法如何工作以对数字进行分类。

    • 与其尝试找到图像与“理想图像”之间的相似性,我们可以查看每个单独的像素,并为每个像素提出一组权重,使得最高的权重与最有可能为特定类别的黑色像素相关联。例如,向右下方的像素不太可能被激活为 7,因此它们对于 7 的权重应该很低,但它们很可能被激活为 8,因此它们对于 8 的权重应该很高。这可以表示为一个函数和每个可能类别的一组权重值,例如,成为数字 8 的概率
  4. 什么是列表推导?现在创建一个从列表中选择奇数并将其加倍的列表推导。

    • new_list = [x*2 for x in a_list if x%2 != 0]
  5. 什么是秩-3 张量?

    • 秩-3 张量是具有三个独立维度的多维数组(形状如 (a, b, c)),可表示多矩阵堆叠或复杂三维数据(如视频帧序列、批量文本的词向量等)。
  6. 张量秩和形状之间有什么区别?如何从形状中获取秩?

    • 张量秩指维度数量(如秩3是三维数组),形状描述各维度长度(如(2,3,4));秩等于形状元组的长度(len(tensor.shape))。
  7. RMSE 和 L1 范数是什么?

    • RMSE(均方根误差)是预测值与真实值误差平方均值的平方根,用于衡量回归模型精度;
    • 真实值:$y_i \in \mathbb{R}$
    • 预测值:$\hat{y}_i \in \mathbb{R}$
    • 样本数:$n \in \mathbb{N}^*$
    • L1范数(如MAE)是误差绝对值的总和,常用于鲁棒性损失函数或稀疏正则化。
    • MAE(平均绝对误差)
  1. 如何才能比 Python 循环快几千倍地一次性对数千个数字进行计算?

    • 将数据转换为 NumPy 数组,利用其底层C语言实现的向量化操作:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      import numpy as np

      # 原生Python循环(慢)
      data = [1, 2, 3, ..., 10000]
      result = [x * 2 + 5 for x in data]

      # NumPy向量化(快几千倍)
      data_np = np.array(data)
      result_np = data_np * 2 + 5 # 无显式循环,逐元素操作
      • 超大规模数据时,通过 GPU 并行计算(需NVIDIA显卡):
        1
        2
        3
        4
        import cupy as cp

        data_gpu = cp.array(data) # 数据传至GPU显存
        result_gpu = cp.exp(data_gpu) * 10 # GPU并行计算指数和乘法
    • 对复杂逻辑,通过 JIT编译 生成机器码加速
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from numba import jit
    import numpy as np

    @jit(nopython=True) # 强制编译为原生机器码
    def process_array(arr):
    result = np.empty_like(arr)
    for i in range(len(arr)):
    result[i] = arr[i] ** 2 + np.sin(arr[i])
    return result

    data = np.random.rand(1000000)
    result = process_array(data) # 首次运行编译,后续调用极快
  2. 创建一个包含从 1 到 9 的数字的 3×3 张量或数组。将其加倍。选择右下角的四个数字。

    • PyTorch
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      import torch

      # 创建 3x3 张量(数值1-9)
      tensor = torch.arange(1, 10).view(3, 3) # tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

      # 所有元素加倍
      doubled_tensor = tensor * 2 # tensor([[ 2, 4, 6], [ 8, 10, 12], [14, 16, 18]])

      # 选择右下角四个数字(最后两行 & 最后两列)
      selected = doubled_tensor[1:, 1:] # tensor([[10, 12], [16, 18]])
    • numpy
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      import numpy as np

      # 创建 3x3 数组(数值1-9)
      array = np.arange(1, 10).reshape(3, 3) # array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

      # 所有元素加倍
      doubled_array = array * 2 # array([[ 2, 4, 6], [ 8, 10, 12], [14, 16, 18]])

      # 选择右下角四个数字
      selected = doubled_array[1:, 1:] # array([[10, 12], [16, 18]])
  3. 广播是什么?

    • 广播是一种重要的功能,使张量代码更容易编写。在广播后,使两个参数张量具有相同的秩后,PyTorch 对于秩相同的两个张量应用其通常的逻辑:它对两个张量的每个对应元素执行操作,并返回张量结果。
  4. 度量通常是使用训练集还是验证集计算的?为什么?

    • 验证集,排除过拟合状态。
  5. SGD 是什么?

    • SGD(随机梯度下降,Stochastic Gradient Descent) 是一种用于优化模型参数的迭代算法,广泛应用于机器学习和深度学习。其核心思想是通过随机选取小批量数据(mini-batch) 计算梯度并更新参数,而非使用全部数据,从而显著提升训练效率。
  6. 为什么 SGD 使用小批量?

    • 利用硬件并行加速,提升计算效率;

    • 平衡梯度噪声与稳定性,避免剧烈震荡;

    • 适应内存限制,灵活处理大规模数据。

  7. SGD 在机器学习中有哪七个步骤?

    • 初始化权重。

    • 对于每个图像,使用这些权重来预测它是 3 还是 7。

    • 基于这些预测,计算模型有多好(它的损失)。

    • 计算梯度,它衡量了每个权重的变化如何改变损失。

    • 根据这个计算,改变(即,改变)所有权重。

    • 回到步骤 2 并重复这个过程。

    • 迭代直到你决定停止训练过程(例如,因为模型已经足够好或者你不想再等待了)。

  8. 我们如何初始化模型中的权重?

    • 我们将参数初始化为随机值。这可能听起来令人惊讶。我们当然可以做其他选择,比如将它们初始化为该类别激活该像素的百分比—但由于我们已经知道我们有一种方法来改进这些权重,结果证明只是从随机权重开始就可以完全正常运行。
  9. 什么是损失?

    • 这就是 Samuel 所说的根据实际表现测试任何当前权重分配的有效性。我们需要一个函数,如果模型的表现好,它将返回一个小的数字(标准方法是将小的损失视为好的,大的损失视为坏的,尽管这只是一种约定)。
  10. 为什么我们不能总是使用高学习率?

    • 如果学习率太高,它也可能会“弹跳”而不是发散;图 4-4 显示了这样做需要许多步骤才能成功训练。
  1. 什么是梯度?

    • SGD中的梯度是损失函数对参数的敏感度,指导参数向损失降低的方向调整。小批量计算兼顾了效率与稳定性,是深度学习的核心驱动力。
  2. 你需要知道如何自己计算梯度吗?

    • 不需要
  3. 为什么我们不能将准确率作为损失函数使用?

    • 关键区别在于指标用于驱动人类理解,而损失用于驱动自动学习。为了驱动自动学习,损失必须是一个具有有意义导数的函数。它不能有大的平坦部分和大的跳跃,而必须是相当平滑的。这就是为什么我们设计了一个损失函数,可以对置信水平的小变化做出响应。这个要求意味着有时它实际上并不完全反映我们试图实现的目标,而是我们真正目标和一个可以使用其梯度进行优化的函数之间的妥协。损失函数是针对数据集中的每个项目计算的,然后在时代结束时,所有损失值都被平均,整体均值被报告为时代。
  4. 绘制 Sigmoid 函数。它的形状有什么特别之处?

    • 正如您所看到的,它接受任何输入值,正数或负数,并将其压缩为 0 和 1 之间的输出值。它还是一个只上升的平滑曲线,这使得 SGD 更容易找到有意义的梯度。

  1. 损失函数和度量之间有什么区别?

    • 损失函数:是模型训练的“指南针”,需可导且适合优化。
      度量:是模型性能的“成绩单”,反映实际任务需求。
      核心原则:损失函数服务于训练过程,度量服务于业务目标,两者需根据任务特性协同设计。
  2. 使用学习率计算新权重的函数是什么?

    参数-=学习率*梯度

  3. DataLoader类是做什么的?

    • 如果我们在训练过程中可以改变一些东西,我们会获得更好的泛化能力。我们可以改变的一个简单而有效的事情是将哪些数据项放入每个小批次。我们通常不是简单地按顺序枚举我们的数据集,而是在每个时代之前随机洗牌,然后创建小批次。PyTorch 和 fastai 提供了一个类,可以为您执行洗牌和小批次整理,称为DataLoader
  4. 编写伪代码,显示每个 epoch 中 SGD 所采取的基本步骤。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    # 初始化模型参数 θ(例如权重矩阵、偏置向量)
    Initialize θ randomly

    # 超参数设置
    learning_rate = 0.01
    batch_size = 32
    num_epochs = 100

    for epoch in 1 to num_epochs:
    # 将训练数据随机打乱(确保样本独立性)
    Shuffle training data

    # 将数据划分为多个小批量(mini-batch)
    for i in 0 to (num_samples / batch_size - 1):
    # 提取当前小批量数据
    batch_X = X_train[i*batch_size : (i+1)*batch_size]
    batch_Y = Y_train[i*batch_size : (i+1)*batch_size]

    # 前向传播:计算当前参数下的预测值
    predictions = forward_pass(θ, batch_X) # 例如 y_pred = θ^T X + b

    # 计算损失函数值(如均方误差、交叉熵)
    loss = compute_loss(predictions, batch_Y) # 例如 L = 1/m Σ(y_pred - y_true)^2

    # 反向传播:计算损失对参数θ的梯度 ∇θ
    gradients = compute_gradients(θ, batch_X, batch_Y) # ∇θ = dL/dθ

    # 更新参数:沿梯度反方向调整θ
    θ = θ - learning_rate * gradients

    # (可选)在每轮结束后计算验证集损失/度量
    val_predictions = forward_pass(θ, X_val)
    val_loss = compute_loss(val_predictions, Y_val)
    print(f"Epoch {epoch}: Train Loss = {loss}, Val Loss = {val_loss}")
  5. 创建一个函数,如果传递两个参数[1,2,3,4]'abcd',则返回[(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]。该输出数据结构有什么特别之处?

    1
    2
    3
    4
    5
    def pair_elements(nums, chars):
    return list(zip(nums, chars))

    # 测试
    print(pair_elements([1,2,3,4], 'abcd')) # 输出 [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]
  6. PyTorch 中的view是做什么的?

    • 我们已经有了我们的x—也就是我们的自变量,图像本身。我们将它们全部连接成一个单一的张量,并且还将它们从矩阵列表(一个秩为 3 的张量)转换为向量列表(一个秩为 2 的张量)。我们可以使用view来做到这一点,view是一个 PyTorch 方法,可以改变张量的形状而不改变其内容。-1view的一个特殊参数,意思是“使这个轴尽可能大以适应所有数据”
  7. 神经网络中的偏差参数是什么?我们为什么需要它们?

    • 神经网络中的偏差参数(Bias)是每个神经元中的一个可学习参数,用于在加权和计算后添加一个常数偏移。

    • 平移激活函数的输入:允许调整加权和的基线位置,使激活函数能适应不同数据分布。增强模型表达能力:没有偏差时,模型只能学习经过原点的超平面;加入偏差后,模型可以表示任意位置的超平面。

  8. Python 中的@运算符是做什么的?

    • 矩阵乘法用@运算符表示
  9. backward方法是做什么的?

    • 这里的backward指的是反向传播,这是计算每一层导数的过程的名称。我们将在第十七章中看到这是如何精确完成的,当我们从头开始计算深度神经网络的梯度时。这被称为网络的反向传播,与前向传播相对,前者是计算激活的地方。如果backward只是被称为calculate_grad,生活可能会更容易,但深度学习的人确实喜欢在任何地方添加行话!
  10. 为什么我们必须将梯度清零?

    • 必须将梯度清零是为了防止不同批次(batch)的梯度在反向传播时累积,导致参数更新方向错误,确保每个批次的梯度独立计算并正确更新模型参数。
  11. 我们需要向Learner传递什么信息?

    • 我们需要传入本章中创建的所有元素:DataLoaders,模型,优化函数(将传递参数),损失函数,以及可选的任何要打印的指标:
  12. 展示训练循环的基本步骤的 Python 或伪代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    # 1. 初始化模型、损失函数、优化器
    model = 初始化神经网络()
    loss_function = 选择损失函数() # 如交叉熵、均方误差
    optimizer = 选择优化器(model.parameters(), 学习率) # 如SGD、Adam

    # 2. 数据预处理
    dataset = 加载数据集()
    dataloader = 分批次(dataset, batch_size=32, shuffle=True)

    # 3. 训练循环(按epoch迭代)
    for epoch in 1到最大训练轮次:
    model.训练模式() # 启用Dropout/BatchNorm等训练特定层

    # 遍历所有小批量(mini-batch)
    for 每个batch的输入数据x, 标签y in dataloader:
    # 3.1 梯度清零(关键!防止梯度累积)
    optimizer.清空梯度()

    # 3.2 前向传播:计算预测值
    预测值 = model.前向计算(x)

    # 3.3 计算损失(预测值与真实值差距)
    loss = loss_function(预测值, y)

    # 3.4 反向传播:计算梯度
    loss.反向传播() # 自动计算各参数梯度

    # 3.5 参数更新:沿梯度反方向调整参数
    optimizer.更新参数()

    # (可选)4. 验证/测试循环
    model.评估模式() # 禁用Dropout/BatchNorm等训练特定层
    total_loss = 0.0
    for 每个验证batch的输入x_val, 标签y_val in 验证集:
    预测_val = model.前向计算(x_val)
    total_loss += loss_function(预测_val, y_val)
    平均验证损失 = total_loss / 验证batch数量
    打印(f"Epoch {epoch}, 验证损失: {平均验证损失}")
  13. ReLU 是什么?为值从-2+2绘制一个图。

    • 那个小函数res.max(tensor(0.0))被称为修正线性单元,也被称为ReLU。我们认为我们都可以同意修正线性单元听起来相当花哨和复杂…但实际上,它不过是res.max(tensor(0.0))——换句话说,用零替换每个负数。这个微小的函数在 PyTorch 中也可以作为F.relu使用:

  1. 什么是激活函数?

    • 神经网络包含多个层。每一层都是线性非线性的。我们通常在神经网络中交替使用这两种类型的层。有时人们将线性层及其后续的非线性一起称为一个单独的层。是的,这很令人困惑。有时非线性被称为激活函数
  2. F.relunn.ReLU之间有什么区别?

    • nn.ReLU:是 模块化的层,适合定义静态模型结构,参数在初始化时固定。
      F.relu:是 函数式接口,适合动态或条件性激活场景,参数在调用时指定。
      当需要将激活函数作为模型的一部分(如保存/加载模型)时,优先用 nn.ReLU。
      当需要灵活控制激活逻辑时,用 F.relu。
  3. 通用逼近定理表明,任何函数都可以使用一个非线性逼近得到所需的精度。那么为什么我们通常使用更多的非线性函数?

    • 在 1990 年代,研究人员如此专注于通用逼近定理,以至于很少有人尝试超过一个非线性。这种理论但不实际的基础阻碍了该领域多年。然而,一些研究人员确实尝试了深度模型,并最终能够证明这些模型在实践中表现得更好。最终,出现了理论结果,解释了为什么会发生这种情况。今天,几乎不可能找到任何人只使用一个非线性的神经网络。

进一步研究

  1. 从头开始创建自己的Learner实现,基于本章展示的训练循环。

  2. 使用完整的 MNIST 数据集完成本章的所有步骤(不仅仅是 3 和 7)。这是一个重要的项目,需要花费相当多的时间来完成!您需要进行一些研究,以找出如何克服在途中遇到的障碍。

前言

正如我们在第一章和第二章中讨论的,有时机器学习模型可能出错。它们可能有错误。它们可能被呈现出以前没有见过的数据,并以我们意料之外的方式行事。或者它们可能完全按设计工作,但被用于我们非常希望它们永远不要被用于的事情。

因为深度学习是如此强大的工具,可以用于很多事情,所以我们特别需要考虑我们选择的后果。哲学上对伦理的研究是对对错的研究,包括我们如何定义这些术语,识别对错行为,以及理解行为和后果之间的联系。数据伦理领域已经存在很长时间,许多学者都专注于这个领域。它被用来帮助定义许多司法管辖区的政策;它被用在大大小小的公司中,考虑如何最好地确保产品开发对社会的良好结果;它被研究人员用来确保他们正在做的工作被用于好的目的,而不是坏的目的。

因此,作为一个深度学习从业者,你很可能在某个时候会面临需要考虑数据伦理的情况。那么数据伦理是什么?它是伦理学的一个子领域,所以让我们从那里开始。

第三章貌似是深度学习的思政课开始就是数据伦理感觉可以摸鱼了。

结论

鉴于我已经在学校修过《工程伦理了》所以让我们跳过这一章

前言

因为在本次课程中本来要使用的的微软的azure服务因为在国外而且服务部署很不稳定无法使用所以打算将文中使用的bing rearch v7换成阿里云的图像搜素服务进行fastai chapter 2的代码演示。所以本次博客记录的是对阿里云图像搜索SDK(阿里云的图像搜索服务在python上并不支持api的使用方式给出的是sdk的使用办法虽然说对于个人用户而言更加的安全高效但是需要对课程中的代码进行大修改)的学习及使用核对课程代码的修改运行。

阿里云python sdk

接口列表

接口名称 接口说明
Add 增加图片。
SearchImageByPro 使用图片进行搜索。
SearchImageByName 指定名称,使用已入库的图片进行搜索。
SearchImageByTotal 使用文本进行搜索。(仅在服务类型为商品名和收藏类时可以使用。)
Delete 删除图片。
UpdateImage 修改图片。
Dictal 查询你的消息。
DumpMeta 无信息导出任务。
DumpMetaList 无信息导出任务列表。
BatchTask 批量任务。
BatchTaskList 批量任务列表。
CompareSimilarByImage 比较所有图片的相似度。

准备工作

  • 在安装和使用阿里云SDK前,确保您已经注册阿里云账号并生成访问密钥(AccessKey)。详情请参见创建AccessKey

  • 使用如下方式安装依赖包。

1
pip install alibabacloud_imagesearch20201214

Add 接口

  • example:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    from alibabacloud_imagesearch20201214.client import Client
    from alibabacloud_imagesearch20201214.models import AddImageAdvanceRequest
    from alibabacloud_tea_openapi.models import Config
    from alibabacloud_tea_util.models import RuntimeOptions
    def addImage():
    request = AddImageAdvanceRequest()
    # 必填,图像搜索实例名称。注意是实例名称不是实例ID。购买后通过上云层管控台实例信息一栏查看:https://imagesearch.console.aliyun.com/overview
    request.instance_name = '<instanceName>'
    # 必填,商品id,最多支持 256个字符。
    # 一个商品可有多张图片。
    request.product_id = '<productId>'
    # 必填,图片名称,最多支持 256个字符。
    # 1. ProductId + PicName唯一确定一张图片。
    # 2. 如果多次添加图片具有相同的ProductId + PicName,以最后一次添加为准,前面添加的图片将被覆盖。
    request.pic_name = '<picName>'
    # 图片内容,最多支持 4MB大小图片以及5s的传输等待时间。当前仅支持PNG、JPG、JPEG、BMP、GIF、WEBP、TIFF、PPM格式图片;
    # 对于商品、商标、通用图片搜索,图片长和宽的像素必须都大于等于100且小于等于4096;
    # 对于布料搜索,图片长和宽的像素必须都大于等于448且小于等于4096;
    # 图像中不能带有旋转信息;

    # 使用URL方式释放下方注释即可。
    # url = '<fileUrl>'
    # f = BytesIO(requests.get(url).content)

    # 使用图片file新增
    f = open('<filePath>', 'rb')

    request.pic_content_object = f
    # 选填,商品类目。
    # 1. 对于商品搜索:若设置类目,则以设置的为准;若不设置类目,将由系统进行类目预测,预测的类目结果可在Response中获取 。
    # 2. 对于布料、商标、通用搜索:不论是否设置类目,系统会将类目设置为88888888。
    request.category_id = 3
    # 选填,用户自定义的内容,最多支持4096个字符。
    # 查询时会返回该字段。例如可添加图片的描述等文本。
    request.custom_content = "this is a simple test"
    # 选填,整数类型属性,可用于查询时过滤,查询时会返回该字段。
    # 例如不同的站点的图片/不同用户的图片,可以设置不同的IntAttr,查询时通过过滤来达到隔离的目的
    request.int_attr = 56
    # 选填,字符串类型属性,最多支持 128个字符。可用于查询时过滤,查询时会返回该字段。
    request.str_attr = "test"
    # 选填,是否需要进行主体识别,默认为true。
    # 1.为true时,由系统进行主体识别,以识别的主体进行搜索,主体识别结果可在Response中获取。
    # 2.为false时,则不进行主体识别,以整张图进行搜索。
    # 3.对于布料图片搜索,此参数会被忽略,系统会以整张图进行搜索。
    request.crop = True
    # 选填,图片的主体区域,格式为 x1,x2,y1,y2, 其中 x1,y1 是左上角的点,x2,y2是右下角的点。设置的region 区域不要超过图片的边界。
    # 若用户设置了Region,则不论Crop参数为何值,都将以用户输入Region进行搜索。
    # 对于布料图片搜索,此参数会被忽略,系统会以整张图进行搜索。
    request.region = "167,467,221,407"
    config = Config()
    # 创建AK/SK参考:https://help.aliyun.com/document_detail/116401.htm
    # 阿里云账号AccessKey拥有所有API的访问权限,建议您使用RAM用户进行API访问或日常运维。
    # 强烈建议不要把AccessKey ID和AccessKey Secret保存到工程代码里,否则可能导致AccessKey泄露,威胁您账号下所有资源的安全。
    # 本示例以将AccessKey ID和AccessKey Secret保存在环境变量为例说明。您也可以根据业务需要,保存到配置文件里。
    config.access_key_id = os.environ['CC_AK_ENV']
    config.access_key_secret = os.environ['CC_SK_ENV']
    # 请更换成您购买实例的区域,例如购买的是杭州区域,则endpoint='imagesearch.cn-hangzhou.aliyuncs.com'
    config.endpoint = 'imagesearch.[regionId].aliyuncs.com'

    # 以下为内网(VPC)访问方式
    # 说明:内网(VPC)访问:仅限同区域ECS或资源的访问,例如您购买的图像搜索实例是华东2(上海),那么您的ECS或资源也必须在华东2(上海)才能通过内网VPC地址访问图搜服务,否则会调用不通,如果遇到调用不通,请先检查您的ECS或资源与图像搜索是否在同一个区域。
    # config.endpointType = 'internal' // 如果是内网访问图像搜索服务,则endpointType为必填项,值统一为'internal'
    # config.endpoint = 'imagesearch-vpc.[regionId].aliyuncs.com' // 为内网访问(VPC)地址,请您更换为您购买实例的区域,例如您购买实例的区域为杭州,则endpoint='imagesearch-vpc.cn-hangzhou.aliyuncs.com'

    # 请您更换成您购买实例的区域,例如您购买的实例区域为杭州,则更换为regionId='cn-hangzhou'
    config.region_id = '<regionId>'
    config.type = 'access_key'
    client = Client(config)
    runtime_option = RuntimeOptions()
    response = client.add_image_advance(request, runtime_option)
    print(response.to_map())
    f.close()
    if __name__ == '__main__':
    addImage()
  • result:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    'RequestId': '7F769FFC-4F45-476E-BE6C-E4EF82E012A7',
    'Success': True,
    'Message': 'success',
    'Code': 0,
    'PicInfo': {
    'CategoryId': 20,
    'Region': '474,747,497,784'
    }
    }

    SearchImagebyName 接口

  • example:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    from alibabacloud_imagesearch20201214.client import Client
    from alibabacloud_imagesearch20201214.models import SearchImageByNameRequest
    from alibabacloud_tea_openapi.models import Config
    from alibabacloud_tea_util.models import RuntimeOptions
    def searchImageByName() :
    request = SearchImageByNameRequest()
    # 必填,图像搜索实例名称。注意是实例名称不是实例ID。购买后通过上云层管控台实例信息一栏查看:https://imagesearch.console.aliyun.com/overview
    request.instance_name = '<instanceName>'
    # 必填,商品id,最多支持 256个字符。
    # 一个商品可有多张图片。
    request.product_id = '<productId>'
    # 必填,图片名称,最多支持 256个字符。
    # 1. ProductId + PicName唯一确定一张图片。
    # 2. 如果多次添加图片具有相同的ProductId + PicName,以最后一次添加为准,前面添加的图片将被覆盖。
    request.pic_name = '<picName>'
    # 选填,商品类目。
    # 1. 对于商品搜索:若设置类目,则以设置的为准;若不设置类目,将由系统进行类目预测,预测的类目结果可在Response中获取 。
    # 2. 对于布料、商标、通用搜索:不论是否设置类目,系统会将类目设置为88888888。
    request.category_id = 3
    # 选填,返回结果的数目。取值范围:1-100。默认值:10。
    request.num = 10
    # 选填,返回结果的起始位置。取值范围:0-499。默认值:0。
    request.start = 0
    # 选填,是否需要进行主体识别,默认为true。
    # 1.为true时,由系统进行主体识别,以识别的主体进行搜索,主体识别结果可在Response中获取。
    # 2.为false时,则不进行主体识别,以整张图进行搜索。
    # 3.对于布料图片搜索,此参数会被忽略,系统会以整张图进行搜索。
    request.crop = True
    # 选填,图片的主体区域,格式为 x1,x2,y1,y2, 其中 x1,y1 是左上角的点,x2,y2是右下角的点。设置的region 区域不要超过图片的边界。
    # 若用户设置了Region,则不论Crop参数为何值,都将以用户输入Region进行搜索。
    # 3.对于布料图片搜索,此参数会被忽略,系统会以整张图进行搜索。
    request.region="167,467,221,407"
    # 选填,过滤条件
    # int_attr支持的操作符有>、>=、<、<=、=,str_attr支持的操作符有=和!=,多个条件之支持AND和OR进行连接。
    # 示例:
    # 1. 根据IntAttr过滤结果,int_attr>=100
    # 2. 根据StrAttr过滤结果,str_attr!="value1"
    # 3. 根据IntAttr和StrAttr联合过滤结果,int_attr=1000 AND str_attr="value1"
    request.filter="int_attr=56 AND str_attr=\"test\""
    config = Config()
    # 创建AK/SK参考:https://help.aliyun.com/document_detail/116401.htm
    # 阿里云账号AccessKey拥有所有API的访问权限,建议您使用RAM用户进行API访问或日常运维。
    # 强烈建议不要把AccessKey ID和AccessKey Secret保存到工程代码里,否则可能导致AccessKey泄露,威胁您账号下所有资源的安全。
    # 本示例以将AccessKey ID和AccessKey Secret保存在环境变量为例说明。您也可以根据业务需要,保存到配置文件里。
    config.access_key_id = os.environ['CC_AK_ENV']
    config.access_key_secret = os.environ['CC_SK_ENV']
    # 请更换成您购买实例的区域,例如购买的是杭州区域,则endpoint='imagesearch.cn-hangzhou.aliyuncs.com'
    config.endpoint = 'imagesearch.<regionId>.aliyuncs.com'

    # 以下为内网(VPC)访问方式
    # 说明:内网(VPC)访问:仅限同区域ECS或资源的访问,例如您购买的图像搜索实例是华东2(上海),那么您的ECS或资源也必须在华东2(上海)才能通过内网VPC地址访问图搜服务,否则会调用不通,如果遇到调用不通,请先检查您的ECS或资源与图像搜索是否在同一个区域。
    # config.endpointType = 'internal' // 如果是内网访问图像搜索服务,则endpointType为必填项,值统一为'internal'
    # config.endpoint = 'imagesearch-vpc.<regionId>.aliyuncs.com' // 为内网访问(VPC)地址,请您更换为您购买实例的区域,例如您购买实例的区域为杭州,则endpoint='imagesearch-vpc.cn-hangzhou.aliyuncs.com'

    # 请您更换成您购买实例的区域,例如您购买的实例区域为杭州,则更换为regionId='cn-hangzhou'
    config.region_id = '<regionId>'
    config.type = 'access_key'
    client = Client(config)
    response = client.search_image_by_name(request)
    print(response.to_map())
    if __name__ == '__main__':
    searchImageByName()
  • result:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
{
'RequestId': '7BC00158-3B9B-49C4-9E25-FFEC28AF3CE8',
'Success': True,
'Code': 0,
'Msg': 'success',
'Auctions': [{
'CategoryId': 20,
'ProductId': 'test-version-001',
'PicName': 'test-version-001.jpg',
'CustomContent': None,
'score':1.0,
'SortExprValues': '5.37633353624177e+24;0',
'IntAttr': None,
'StrAttr': None
}, {
'CategoryId': 20,
'ProductId': 'test_0426_1',
'PicName': 'test_0426_1.png',
'CustomContent': None,
'score':1.0,
'SortExprValues': '2.71303606033325;263',
'IntAttr': None,
'StrAttr': None
}],
'Head': {
'DocsReturn': 5,
'DocsFound': 5,
'SearchTime': 15
},
'PicInfo': {
'CategoryId': 20,
'Region': None,
'AllCategories': [{
'Id': 0,
'Name': 'Tops'
}, {
'Id': 1,
'Name': 'Dress'
}, {
'Id': 2,
'Name': 'Bottoms'
}, {
'Id': 3,
'Name': 'Bag'
}, {
'Id': 4,
'Name': 'Shoes'
}, {
'Id': 5,
'Name': 'Accessories'
}, {
'Id': 6,
'Name': 'Snack'
}, {
'Id': 7,
'Name': 'Makeup'
}, {
'Id': 8,
'Name': 'Bottle'
}, {
'Id': 9,
'Name': 'Furniture'
}, {
'Id': 20,
'Name': 'Toy'
}, {
'Id': 21,
'Name': 'Underwear'
}, {
'Id': 22,
'Name': 'Digital device'
}, {
'Id': 88888888,
'Name': 'Other'
}],
'MultiRegion': [{
'Region': '112,440,76,387'
}]
}
}

关于Colab

Google Colab是谷歌提供的免费Jupyter 笔记本环境,不需要什么设置与环境配置就可以使用,完全在云端运行。不影响本地的使用。

Google Colab为研究者提供一定免费的GPU,可以编写和执行代码,所有这些都可通过浏览器免费使用。同学们可以在上面轻松地跑 Tensorflow、Pytorch 等深度学习框架。

尽管Google Colab提供了一定的免费资源,但资源量是受限制的,所有 Colab 运行时都会在一段时间后重置。Colab Pro 订阅者的使用量仍会受到限制,但相比非订阅者可享有的限额要多出大约一倍。Colab Pro+ 订阅者还可获享更高的稳定性。

关于Google Driver

google云端硬盘,也称为谷歌云端硬盘,是互联网巨头谷歌公司推出的一项在线云存储服务,英文全称是Google Drive。目前有免费和付费两种模式,免费用户可以获取15G的空间,付费用户根据套餐可以选择最大20TB的储存空间

利用colab进行fastai课程中的代码运行

因为本来为教学课程为了快速上手并没有充分的时间进行环境的配置,同时我的gpu也并非一个较好的水位(rtx2060),加上之前在复现论文时用过colab的免费服务,被google的慷慨和大方震惊,故在进行deeping learning的课程学习时也准备使用colab进行代码的操作。

step 1:在github fastbook项目主页下选择对应的学习章节

选择对应的学习章节进行打开即可转移到相应的项目colab。

github主页课程选择

需要注意这个课程选择的主页仅当主页语言为英文时才会出现。

step2 :在进入colab页面后选择托管硬件为gpu

点击 代码执行程序 —> 更改运行时类型 —>选择 T4 GPU(当然土豪是随意 充钱即可享受更强大的gpu)

最后进行连接,点击需要测试的代码块,即可线上进行代码的测试

colab设置

colab测试运行结果

在运行安装环境配置的代码块时出现了报错

error

在forums.fast.ai论坛上进行查找发现该问题并不影响后续程序的执行

anser

执行第一个进行cnn网络训练得到猫狗识别的模型的程序结果

result

总结

完成了深度学习的重要的一步在线上对代码进行运行,在目前算力设备都极其昂贵的背景下,选择线上的代码服务进行模型的训练显得尤其具有性价比,掌握该项技能能够使得我的科研和工作突破某些因为算力不足的限制,同时对于学习更加的方便。

前言

本章主要探讨的是深度学习的实践,我们需要知道深度学习的能力和限制,以至于让我们我至于低估或是对深度学习的能力和限制有过高的期盼。

从模型到生成

在本章中,我们将使用一个计算机视觉示例来查看创建深度学习应用的端到端过程。更具体地说,我们将构建一个熊分类器!在这个过程中,我们将讨论深度学习的能力和限制,探讨如何创建数据集,在实践中使用深度学习时可能遇到的问题等等。许多关键点同样适用于其他深度学习问题,例如第一章中的问题。如果您解决的问题在关键方面类似于我们的示例问题,我们期望您可以快速获得极好的结果,而只需很少的代码。

开始深度学习的项目

首先开始深度学习像是在制作一份独属于你自己的三明治!(尽管他可能没有那么好吃!但是你必须会自己做它!)只有通过处理自己的项目,您才能获得构建和使用模型的真实经验。

就像做三明治一样,你不可能找到一块完美的火腿或者是生菜进行制作。(一个切的薄厚不均的火腿片或是不太完整的生菜可能并不会影响三明治的味道所以告别完美主义不要让你的工作停滞不前)

无论您是为了自己的学习还是为了在组织中的实际应用而进行项目,您都希望能够快速开始。我们看到许多学生、研究人员和行业从业者在试图找到他们完美的数据集时浪费了几个月甚至几年的时间。目标不是找到“完美”的数据集或项目,而只是开始并从那里迭代。如果您采取这种方法,您将在完美主义者仍处于规划阶段时进行第三次迭代学习和改进!

我们还建议您在项目中端到端迭代;不要花几个月来微调您的模型,或打磨完美的 GUI,或标记完美的数据集……相反,尽可能在合理的时间内完成每一步,一直到最后。例如,如果您的最终目标是一个在手机上运行的应用程序,那么每次迭代后您都应该拥有这个。但也许在早期迭代中您会采取捷径;例如,在远程服务器上进行所有处理,并使用简单的响应式 Web 应用程序。通过完成项目的端到端,您将看到最棘手的部分在哪里,以及哪些部分对最终结果产生最大影响。

Sylvain’s tips

刚开始的时候应该选择一个熟悉的你能接触到大量数据的领域,如果没有也应该选择一个已经被应用于深度学习的领域。不然你就会和无头苍蝇一样不知道自己错在哪里。

深度学习应用的领域分析(书的日期为2020年但现在是2025年我会结合自己的一些项目经历进行分析)

计算机视觉

书中说到:深度学习尚未用于分析图像的许多领域,但在已经尝试过的领域中,几乎普遍表明计算机可以至少与人类一样好地识别图像中的物品,甚至是经过专门训练的人,如放射科医生。这被称为物体识别。深度学习还擅长识别图像中物体的位置,并可以突出它们的位置并命名每个找到的物体。这被称为物体检测(在我们在第一章中看到的变体中,每个像素根据其所属的对象类型进行分类—这被称为分割)。

深度学习算法通常不擅长识别结构或风格与用于训练模型的图像明显不同的图像。例如,如果训练数据中没有黑白图像,模型可能在黑白图像上表现不佳。同样,如果训练数据不包含手绘图像,模型可能在手绘图像上表现不佳。没有一般方法可以检查训练集中缺少哪些类型的图像,但我们将在本章中展示一些方法,以尝试识别当模型在生产中使用时数据中出现意外图像类型的情况(这被称为检查域外数据)。

物体检测系统面临的一个主要挑战是图像标记可能会很慢且昂贵。目前有很多工作正在进行中,旨在开发工具以尝试使这种标记更快速、更容易,并且需要更少的手工标签来训练准确的物体检测模型。一个特别有帮助的方法是合成生成输入图像的变化,例如通过旋转它们或改变它们的亮度和对比度;这被称为数据增强,并且对文本和其他类型的模型也很有效。我们将在本章中详细讨论这一点。

另一个要考虑的问题是,尽管您的问题可能看起来不像是一个计算机视觉问题,但通过一点想象力可能可以将其转变为一个。例如,如果您要分类的是声音,您可以尝试将声音转换为其声学波形的图像,然后在这些图像上训练模型。

但是实际上现在深度学习在计算机视觉的领域目前在繁荣发展就我而言所使用过的yolo视觉模型简直的惊为天人的存在

自然语言处理

计算机擅长基于类别对短文档和长文档进行分类,例如垃圾邮件或非垃圾邮件、情感(例如,评论是积极的还是消极的)、作者、来源网站等。我们不知道在这个领域是否有任何严格的工作来比较计算机和人类,但从经验上看,我们认为深度学习的性能在这些任务上与人类的性能相似。

深度学习还擅长生成与上下文相关的文本,例如回复社交媒体帖子,并模仿特定作者的风格。它还擅长使这些内容对人类具有吸引力—事实上,甚至比人类生成的文本更具吸引力。然而,深度学习不擅长生成正确的回应!例如,我们没有可靠的方法来将医学信息知识库与深度学习模型结合起来,以生成医学上正确的自然语言回应。这是危险的,因为很容易创建对外行人看来具有吸引力但实际上完全不正确的内容。

另一个问题是,社交媒体上的上下文适当、高度引人入胜的回应可能被大规模使用——比以前见过的任何喷子农场规模大几千倍——来传播虚假信息,制造动荡,鼓励冲突。一般来说,文本生成模型总是在技术上略领先于识别自动生成文本的模型。例如,可以使用一个能够识别人工生成内容的模型来实际改进创建该内容的生成器,直到分类模型无法完成其任务为止。

尽管存在这些问题,深度学习在自然语言处理中有许多应用:可以用来将文本从一种语言翻译成另一种语言,将长篇文档总结为更快消化的内容,找到感兴趣概念的所有提及等。不幸的是,翻译或总结可能包含完全错误的信息!然而,性能已经足够好,许多人正在使用这些系统——例如,谷歌的在线翻译系统(以及我们所知道的每个其他在线服务)都是基于深度学习的。

原来早在深度学习提出之除就发现其在大语言模型上的潜力更加强大,要是早读这本书我就早买大语言模型企业的股票飞黄腾达了,果然还是要多看书QAQ

结合文本和图像

深度学习将文本和图像结合成一个单一模型的能力通常比大多数人直觉期望的要好得多。例如,一个深度学习模型可以在输入图像上进行训练,输出用英语编写的标题,并且可以学会为新图像自动生成令人惊讶地适当的标题!但是,我们再次提出与前一节讨论的相同警告:不能保证这些标题是正确的。

由于这个严重问题,我们通常建议深度学习不要作为完全自动化的过程,而是作为模型和人类用户密切互动的过程的一部分。这可能使人类的生产力比完全手动方法高出几个数量级,并且比仅使用人类更准确。

例如,自动系统可以直接从 CT 扫描中识别潜在的中风患者,并发送高优先级警报,以便快速查看这些扫描。治疗中风只有三个小时的时间窗口,因此这种快速的反馈循环可以挽救生命。同时,所有扫描仍然可以按照通常的方式发送给放射科医生,因此不会减少人类的参与。其他深度学习模型可以自动测量扫描中看到的物品,并将这些测量结果插入报告中,警告放射科医生可能错过的发现,并告诉他们可能相关的其他病例。

这是我作为研究生阶段最关心的点,讲真的把这个用在机器人身上非常的酷

表格数据

对于分析时间序列和表格数据,深度学习最近取得了巨大进展。然而,深度学习通常作为多种模型集成的一部分使用。如果您已经有一个正在使用随机森林或梯度提升机(流行的表格建模工具,您很快将了解)的系统,那么切换到或添加深度学习可能不会带来任何显著的改进。

深度学习确实大大增加了您可以包含的列的种类——例如,包含自然语言(书名、评论等)和高基数分类列(即包含大量离散选择的内容,如邮政编码或产品 ID)。不过,与随机森林或梯度提升机相比,深度学习模型通常需要更长的训练时间,尽管由于提供 GPU 加速的库(如RAPIDS),情况正在改变。我们在第九章中详细介绍了所有这些方法的优缺点。

或许等我走头无路开路边摊的时候可以设计一个来控制我的成本

推荐系统

推荐系统实际上只是一种特殊类型的表格数据。特别是,它们通常具有代表用户的高基数分类变量,以及代表产品(或类似物品)的另一个变量。像亚马逊这样的公司将客户所做的每一次购买都表示为一个巨大的稀疏矩阵,其中客户是行,产品是列。一旦他们以这种格式拥有数据,数据科学家们会应用某种形式的协同过滤来填充矩阵。例如,如果客户 A 购买产品 1 和 10,客户 B 购买产品 1、2、4 和 10,引擎将推荐 A 购买 2 和 4。

由于深度学习模型擅长处理高基数分类变量,它们非常擅长处理推荐系统。尤其是当将这些变量与其他类型的数据(如自然语言或图像)结合时,它们就像处理表格数据一样发挥作用。它们还可以很好地将所有这些类型的信息与其他元数据(如用户信息、先前交易等)表示为表格进行组合。

然而,几乎所有的机器学习方法都有一个缺点,那就是它们只告诉你一个特定用户可能喜欢哪些产品,而不是对用户有用的推荐。用户可能喜欢的产品的许多种推荐可能根本不会有任何帮助——例如,如果用户已经熟悉这些产品,或者如果它们只是用户已经购买过的产品的不同包装(例如,当他们已经拥有该套装中的每一件物品时,推荐一个小说的套装)。Jeremy 喜欢读特里·普拉切特的书,有一段时间亚马逊一直在向他推荐特里·普拉切特的书,这实际上并不是有用的,因为他已经知道这些书了!

愚蠢的b站视频推荐算法就是这样的。我都已经考上了研究生他还一直给我推考研的视频

其他数据类型

通常,您会发现特定领域的数据类型非常适合现有的类别。例如,蛋白质链看起来很像自然语言文档,因为它们是由复杂关系和意义贯穿整个序列的离散令牌组成的长序列。事实上,使用 NLP 深度学习方法是许多类型蛋白质分析的最先进方法。另一个例子,声音可以表示为频谱图,可以被视为图像;标准的图像深度学习方法在频谱图上表现得非常好。

讲真的这个非常的神奇感觉生物和控制结合在一起大有所为!!!快去买生物科技公司的股票!!!!

驱动系统方法

许多准确的模型对任何人都没有用,而许多不准确的模型却非常有用。为了确保您的建模工作在实践中有用,您需要考虑您的工作将如何使用。2012 年,Jeremy 与 Margit Zwemer 和 Mike Loukides 一起提出了一种称为驱动系统方法的思考这个问题的方法。

驱动系统方法,如图 2-2 所示,详细介绍在“设计出色的数据产品”中。基本思想是从考虑您的目标开始,然后考虑您可以采取哪些行动来实现该目标以及您拥有的(或可以获取的)可以帮助的数据,然后构建一个模型,您可以使用该模型确定为实现目标而采取的最佳行动。

图 2-2. 驱动系统方法

考虑自动驾驶汽车中的模型:您希望帮助汽车安全地从 A 点驾驶到 B 点,而无需人为干预。出色的预测建模是解决方案的重要组成部分,但它并不是独立存在的;随着产品变得更加复杂,它会消失在管道中。使用自动驾驶汽车的人完全不知道使其运行的数百(甚至数千)个模型和海量数据。但随着数据科学家构建越来越复杂的产品,他们需要一种系统化的设计方法。

我们使用数据不仅仅是为了生成更多数据(以预测的形式),而是为了产生可操作的结果。这是 Drivetrain 方法的目标。首先要明确定义一个明确的目标。例如,当谷歌创建其第一个搜索引擎时,考虑了“用户在输入搜索查询时的主要目标是什么?”这导致了谷歌的目标,即“显示最相关的搜索结果”。下一步是考虑您可以拉动的杠杆(即您可以采取的行动)以更好地实现该目标。在谷歌的情况下,这是搜索结果的排名。第三步是考虑他们需要什么新数据来生成这样的排名;他们意识到关于哪些页面链接到哪些其他页面的隐含信息可以用于此目的。

只有在完成了这前三个步骤之后,我们才开始考虑构建预测模型。我们的目标和可用的杠杆,我们已经拥有的数据以及我们需要收集的额外数据,决定了我们可以构建的模型。这些模型将以杠杆和任何不可控变量作为输入;模型的输出可以结合起来预测我们的目标的最终状态。

让我们考虑另一个例子:推荐系统。推荐引擎的目标是通过推荐客户不会在没有推荐的情况下购买的物品来推动额外的销售。杠杆是推荐的排名。必须收集新数据以生成将导致新销售的推荐。这将需要进行许多随机实验,以收集关于各种客户的各种推荐的数据。这是很少有组织采取的一步;但是没有它,您就没有所需的信息来根据您的真正目标(更多销售!)优化推荐。

最后,您可以为购买概率构建两个模型,条件是看到或没有看到推荐。这两个概率之间的差异是给定推荐给客户的效用函数。在算法推荐客户已经拒绝的熟悉书籍(两个组成部分都很小)或者他们本来就会购买的书籍(两个组成部分都很大并互相抵消)的情况下,效用函数会很低。

正如您所看到的,在实践中,您的模型的实际实施通常需要比仅仅训练一个模型更多!您通常需要运行实验来收集更多数据,并考虑如何将您的模型整合到您正在开发的整个系统中。说到数据,现在让我们专注于如何为您的项目找到数据。

保持自己能够获取互联网图像

这里需要高校验证来获取免费的Azure的bing图像搜索api的服务(今天才发现我没有高校邮箱现在申请不知道几天可以申请下来)。所以下面需要用到api的代码我就不运行了。欠着等申请下了在运行

1
key = 'XXX'

或者,如果您在命令行上感到自在,您可以在终端中设置它

1
export AZURE_SEARCH_KEY=*your_key_here*

然后重新启动 Jupyter 服务器,在一个单元格中键入以下内容,并执行:

1
key = os.environ['AZURE_SEARCH_KEY']

设置了key之后,您可以使用search_images_bing。这个函数是在线笔记本中包含的小utils类提供的(如果您不确定一个函数是在哪里定义的,您可以在笔记本中输入它来找出,如下所示):

1
search_images_bing
1
<function utils.search_images_bing(key, term, min_sz=128)>

让我们尝试一下这个函数:

1
2
3
results = search_images_bing(key, 'grizzly bear')
ims = results.attrgot('content_url')
len(ims)
1
150

我们已成功下载了 150 只灰熊的 URL(或者至少是 Bing 图像搜索为该搜索词找到的图像)。让我们看一个:

1
2
dest = 'images/grizzly.jpg'
download_url(ims[0], dest)
1
2
im = Image.open(dest)
im.to_thumb(128,128)

这似乎运行得很好,所以让我们使用 fastai 的download_images来下载每个搜索词的所有 URL。我们将每个放在一个单独的文件夹中:

1
2
bear_types = 'grizzly','black','teddy'
path = Path('bears')
1
2
3
4
5
6
7
if not path.exists():
path.mkdir()
for o in bear_types:
dest = (path/o)
dest.mkdir(exist_ok=True)
results = search_images_bing(key, f'{o} bear')
download_images(dest, urls=results.attrgot('content_url'))

我们的文件夹中有图像文件,正如我们所期望的那样:

1
2
fns = get_image_files(path)
fns
1
2
3
4
5
(#421) [Path('bears/black/00000095.jpg'),Path('bears/black/00000133.jpg'),Path('
> bears/black/00000062.jpg'),Path('bears/black/00000023.jpg'),Path('bears/black
> /00000029.jpg'),Path('bears/black/00000094.jpg'),Path('bears/black/00000124.j
> pg'),Path('bears/black/00000056.jpeg'),Path('bears/black/00000046.jpg'),Path(
> 'bears/black/00000045.jpg')...]

Jeremy 说

我就是喜欢在 Jupyter 笔记本中工作的这一点!逐步构建我想要的东西并在每一步检查我的工作是如此容易。我犯了很多错误,所以这对我真的很有帮助。

通常当我们从互联网下载文件时,会有一些文件损坏。让我们检查一下:

1
2
failed = verify_images(fns)
failed
1
(#0) []

要删除所有失败的图像,您可以使用unlink。像大多数返回集合的 fastai 函数一样,verify_images返回一个类型为L的对象,其中包括map方法。这会在集合的每个元素上调用传递的函数:

1
failed.map(Path.unlink);

在这个过程中要注意的一件事是:正如我们在第一章中讨论的,模型只能反映用于训练它们的数据。而世界充满了有偏见的数据,这最终会反映在,例如,Bing 图像搜索(我们用来创建数据集的)。例如,假设您有兴趣创建一个应用程序,可以帮助用户确定他们是否拥有健康的皮肤,因此您训练了一个模型,该模型基于搜索结果(比如)“健康皮肤”。图 2-3 展示了您将获得的结果类型。

图 2-3. 用于健康皮肤检测器的数据?

使用此作为训练数据,您最终不会得到一个健康皮肤检测器,而是一个年轻白人女性触摸她的脸检测器!一定要仔细考虑您可能在应用程序中实际看到的数据类型,并仔细检查以确保所有这些类型都反映在您模型的源数据中。(感谢 Deb Raji 提出了健康皮肤的例子。请查看她的论文“可操作的审计:调查公开命名商业 AI 产品偏见性能结果的影响”以获取更多有关模型偏见的迷人见解。)

现在我们已经下载了一些数据,我们需要将其组装成适合模型训练的格式。在 fastai 中,这意味着创建一个名为DataLoaders的对象。

术语:DataLoaders

一个 fastai 类,存储您传递给它的多个DataLoader对象——通常是一个train和一个valid,尽管可以有任意数量。前两个作为属性提供。

在本书的后面,您还将了解DatasetDatasets类,它们具有相同的关系。要将我们下载的数据转换为DataLoaders对象,我们至少需要告诉 fastai 四件事:

  • 我们正在处理什么类型的数据

  • 如何获取项目列表

  • 如何为这些项目打标签

  • 如何创建验证集

到目前为止,我们已经看到了一些特定组合的工厂方法,当您有一个应用程序和数据结构恰好适合这些预定义方法时,这些方法非常方便。当您不适用时,fastai 有一个名为数据块 API的极其灵活的系统。使用此 API,您可以完全自定义创建DataLoaders的每个阶段。这是我们需要为刚刚下载的数据集创建DataLoaders的步骤:

1
2
3
4
5
6
bears = DataBlock(
blocks=(ImageBlock, CategoryBlock),
get_items=get_image_files,
splitter=RandomSplitter(valid_pct=0.2, seed=42),
get_y=parent_label,
item_tfms=Resize(128))

让我们依次查看每个参数。首先,我们提供一个元组,指定我们希望独立变量和因变量的类型:

1
blocks=(ImageBlock, CategoryBlock)

独立变量是我们用来进行预测的东西,因变量是我们的目标。在这种情况下,我们的独立变量是一组图像,我们的因变量是每个图像的类别(熊的类型)。在本书的其余部分中,我们将看到许多其他类型的块。

对于这个DataLoaders,我们的基础项目将是文件路径。我们必须告诉 fastai 如何获取这些文件的列表。get_image_files函数接受一个路径,并返回该路径中所有图像的列表(默认情况下递归):

1
get_items=get_image_files

通常,您下载的数据集已经定义了验证集。有时,这是通过将用于训练和验证集的图像放入不同的文件夹中来完成的。有时,这是通过提供一个 CSV 文件,在该文件中,每个文件名都与应该在其中的数据集一起列出。有许多可以完成此操作的方法,fastai 提供了一种通用方法,允许您使用其预定义类之一或编写自己的类。

在这种情况下,我们希望随机拆分我们的训练和验证集。但是,我们希望每次运行此笔记本时都具有相同的训练/验证拆分,因此我们固定随机种子(计算机实际上不知道如何创建随机数,而只是创建看起来随机的数字列表;如果您每次都为该列表提供相同的起始点——称为种子,那么您将每次都获得完全相同的列表)。

1
splitter=RandomSplitter(valid_pct=0.2, seed=42)

自变量通常被称为x,因变量通常被称为y。在这里,我们告诉 fastai 要调用哪个函数来创建数据集中的标签:

1
get_y=parent_label

parent_label是 fastai 提供的一个函数,它简单地获取文件所在文件夹的名称。因为我们将每个熊图像放入基于熊类型的文件夹中,这将为我们提供所需的标签。

我们的图像大小各不相同,这对深度学习是一个问题:我们不是一次向模型提供一个图像,而是多个图像(我们称之为mini-batch)。为了将它们分组到一个大数组(通常称为张量)中,以便通过我们的模型,它们都需要是相同的大小。因此,我们需要添加一个转换,将这些图像调整为相同的大小。Item transforms是在每个单独项目上运行的代码片段,无论是图像、类别还是其他。fastai 包含许多预定义的转换;我们在这里使用Resize转换,并指定大小为 128 像素:

1
item_tfms=Resize(128)

这个命令给了我们一个DataBlock对象。这就像创建DataLoaders模板。我们仍然需要告诉 fastai 我们数据的实际来源——在这种情况下,图像所在的路径:

1
dls = bears.dataloaders(path)

DataLoaders包括验证和训练DataLoaderDataLoader是一个类,它一次向 GPU 提供几个项目的批次。我们将在下一章中更多地了解这个类。当您循环遍历DataLoader时,fastai 会一次给您 64 个(默认值)项目,全部堆叠到一个单一张量中。我们可以通过在DataLoader上调用show_batch方法来查看其中一些项目:

1
dls.valid.show_batch(max_n=4, nrows=1)

默认情况下,Resize会将图像裁剪成适合请求大小的正方形形状,使用完整的宽度或高度。这可能会导致丢失一些重要细节。或者,您可以要求 fastai 用零(黑色)填充图像,或者压缩/拉伸它们:

1
2
3
bears = bears.new(item_tfms=Resize(128, ResizeMethod.Squish))
dls = bears.dataloaders(path)
dls.valid.show_batch(max_n=4, nrows=1)

1
2
3
bears = bears.new(item_tfms=Resize(128, ResizeMethod.Pad, pad_mode='zeros'))
dls = bears.dataloaders(path)
dls.valid.show_batch(max_n=4, nrows=1)

所有这些方法似乎都有些浪费或问题。如果我们压缩或拉伸图像,它们最终会变成不现实的形状,导致模型学习到事物看起来与实际情况不同,这会导致更低的准确性。如果我们裁剪图像,我们会移除一些允许我们进行识别的特征。例如,如果我们试图识别狗或猫的品种,我们可能会裁剪掉区分相似品种所需的身体或面部的关键部分。如果我们填充图像,就会有很多空白空间,这对我们的模型来说只是浪费计算,并导致我们实际使用的图像部分具有较低的有效分辨率。

相反,我们在实践中通常做的是随机选择图像的一部分,然后裁剪到该部分。在每个纪元(即数据集中所有图像的完整遍历),我们随机选择每个图像的不同部分。这意味着我们的模型可以学习关注和识别图像中的不同特征。这也反映了图像在现实世界中的工作方式:同一物体的不同照片可能以略有不同的方式构图。

事实上,一个完全未经训练的神经网络对图像的行为一无所知。它甚至不认识当一个物体旋转一度时,它仍然是同一物体的图片!因此,通过训练神经网络使用物体在略有不同位置并且大小略有不同的图像的示例,有助于它理解物体的基本概念,以及如何在图像中表示它。

这里是另一个示例,我们将Resize替换为RandomResizedCrop,这是提供刚才描述行为的转换。传递的最重要参数是min_scale,它确定每次选择图像的最小部分:

1
2
3
bears = bears.new(item_tfms=RandomResizedCrop(128, min_scale=0.3))
dls = bears.dataloaders(path)
dls.train.show_batch(max_n=4, nrows=1, unique=True)

在这里,我们使用了unique=True,以便将相同图像重复使用不同版本的RandomResizedCrop变换。

RandomResizedCrop是更一般的数据增强技术的一个具体示例。

数据增强

数据增强指的是创建输入数据的随机变化,使它们看起来不同但不改变数据的含义。对于图像的常见数据增强技术包括旋转、翻转、透视变形、亮度变化和对比度变化。对于我们在这里使用的自然照片图像,我们发现一组标准的增强技术与aug_transforms函数一起提供,效果非常好。

因为我们的图像现在都是相同大小,我们可以使用 GPU 将这些增强应用于整个批次的图像,这将节省大量时间。要告诉 fastai 我们要在批次上使用这些变换,我们使用batch_tfms参数(请注意,在此示例中我们没有使用RandomResizedCrop,这样您可以更清楚地看到差异;出于同样的原因,我们使用了默认值的两倍的增强量):

1
2
3
bears = bears.new(item_tfms=Resize(128), batch_tfms=aug_transforms(mult=2))
dls = bears.dataloaders(path)
dls.train.show_batch(max_n=8, nrows=2, unique=True)

现在我们已经将数据组装成适合模型训练的格式,让我们使用它来训练一个图像分类器。

训练您的模型,并使用它来清理您的数据

现在是时候使用与第一章中相同的代码行来训练我们的熊分类器了。对于我们的问题,我们没有太多的数据(每种熊最多 150 张图片),因此为了训练我们的模型,我们将使用RandomResizedCrop,图像大小为 224 像素,这对于图像分类来说是相当标准的,并且使用默认的aug_transforms

1
2
3
4
bears = bears.new(
item_tfms=RandomResizedCrop(224, min_scale=0.5),
batch_tfms=aug_transforms())
dls = bears.dataloaders(path)

现在我们可以按照通常的方式创建我们的Learner并进行微调:

1
2
learn = cnn_learner(dls, resnet18, metrics=error_rate)
learn.fine_tune(4)
epoch train_loss valid_loss error_rate time
0 1.235733 0.212541 0.087302 00:05
epoch train_loss valid_loss error_rate time
—- —- —- —- —-
0 0.213371 0.112450 0.023810 00:05
1 0.173855 0.072306 0.023810 00:06
2 0.147096 0.039068 0.015873 00:06
3 0.123984 0.026801 0.015873 00:06

现在让我们看看模型犯的错误主要是认为灰熊是泰迪熊(这对安全性来说是不好的!),还是认为灰熊是黑熊,或者其他情况。为了可视化这一点,我们可以创建一个混淆矩阵

1
2
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix()

行代表数据集中所有黑色、灰熊和泰迪熊,列分别代表模型预测为黑色、灰熊和泰迪熊的图像。因此,矩阵的对角线显示了被正确分类的图像,而非对角线的单元格代表被错误分类的图像。这是 fastai 允许您查看模型结果的许多方式之一。当然,这是使用验证集计算的。通过颜色编码,目标是在对角线以外的地方都是白色,而在对角线上我们希望是深蓝色。我们的熊分类器几乎没有犯错!

看到我们的错误发生在哪里是有帮助的,以便确定它们是由数据集问题(例如,根本不是熊的图像,或者标记错误)还是模型问题(也许它无法处理使用不同光照或从不同角度拍摄的图像等)。为了做到这一点,我们可以根据损失对图像进行排序。

损失是一个数字,如果模型不正确(尤其是如果它对其不正确的答案也很自信),或者如果它是正确的但对其正确答案不自信,那么损失就会更高。在第二部分的开头,我们将深入学习损失是如何计算和在训练过程中使用的。现在,plot_top_losses向我们展示了数据集中损失最高的图像。正如输出的标题所说,每个图像都标有四个内容:预测、实际(目标标签)、损失和概率。这里的概率是模型对其预测分配的置信水平,从零到一:

1
interp.plot_top_losses(5, nrows=1)

这个输出显示,损失最高的图像是一个被预测为“灰熊”的图像,且置信度很高。然而,根据我们的必应图像搜索,它被标记为“黑熊”。我们不是熊专家,但在我们看来,这个标签显然是错误的!我们可能应该将其标签更改为“灰熊”。

进行数据清洗的直观方法是在训练模型之前进行。但正如您在本例中所看到的,模型可以帮助您更快速、更轻松地找到数据问题。因此,我们通常更喜欢先训练一个快速简单的模型,然后使用它来帮助我们进行数据清洗。

fastai 包括一个方便的用于数据清洗的 GUI,名为ImageClassifierCleaner,它允许您选择一个类别和训练与验证集,并查看损失最高的图像(按顺序),以及菜单允许选择要删除或重新标记的图像:

1
2
cleaner = ImageClassifierCleaner(learn)
cleaner

清洁工具小部件

我们可以看到在我们的“黑熊”中有一张包含两只熊的图片:一只灰熊,一只黑熊。因此,我们应该在此图片下的菜单中选择<Delete>ImageClassifierCleaner不会为您删除或更改标签;它只会返回要更改的项目的索引。因此,例如,要删除(取消链接)所有选定要删除的图像,我们将运行以下命令:

1
for idx in cleaner.delete(): cleaner.fns[idx].unlink()

要移动我们选择了不同类别的图像,我们将运行以下命令:

1
for idx,cat in cleaner.change(): shutil.move(str(cleaner.fns[idx]), path/cat)

Sylvain’s tips

清理数据并为您的模型做好准备是数据科学家面临的两个最大挑战;他们说这需要他们 90%的时间。fastai 库旨在提供尽可能简单的工具。

在本书中,我们将看到更多基于模型驱动的数据清洗示例。一旦我们清理了数据,我们就可以重新训练我们的模型。自己尝试一下,看看你的准确性是否有所提高!

不需要大数据

通过这些步骤清理数据集后,我们通常在这个任务上看到 100%的准确性。即使我们下载的图像比我们在这里使用的每类 150 张要少得多,我们也能看到这个结果。正如您所看到的,您需要大量数据才能进行深度学习的常见抱怨可能与事实相去甚远!

现在我们已经训练了我们的模型,让我们看看如何部署它以便在实践中使用。

将您的模型转化为在线应用程序

现在我们将看看将这个模型转化为一个可工作的在线应用程序需要什么。我们将只创建一个基本的工作原型;在本书中,我们没有范围来教授您有关 Web 应用程序开发的所有细节。

使用模型进行推断

一旦您拥有一个满意的模型,您需要保存它,以便随后将其复制到一个服务器上,在那里您将在生产中使用它。请记住,模型由两部分组成:架构和训练的参数。保存模型的最简单方法是保存这两部分,因为这样,当您加载模型时,您可以确保具有匹配的架构和参数。要保存这两部分,请使用export方法。

这种方法甚至保存了如何创建您的DataLoaders的定义。这很重要,因为否则您将不得不重新定义如何转换您的数据以便在生产中使用您的模型。fastai 默认使用验证集DataLoader进行推理,因此不会应用数据增强,这通常是您想要的。

当您调用export时,fastai 将保存一个名为export.pkl的文件:

1
learn.export()

让我们通过使用 fastai 添加到 Python 的Path类的ls方法来检查文件是否存在:

1
2
path = Path()
path.ls(file_exts='.pkl')
1
(#1) [Path('export.pkl')]

您需要这个文件在您部署应用程序的任何地方。现在,让我们尝试在我们的笔记本中创建一个简单的应用程序。

当我们使用模型进行预测而不是训练时,我们称之为推理。要从导出的文件创建我们的推理学习者,我们使用load_learner(在这种情况下,这并不是真正必要的,因为我们已经在笔记本中有一个工作的Learner;我们在这里这样做是为了让您看到整个过程的始终):

1
learn_inf = load_learner(path/'export.pkl')

在进行推理时,通常一次只为一个图像获取预测。要做到这一点,将文件名传递给predict

1
learn_inf.predict('images/grizzly.jpg')
1
('grizzly', tensor(1), tensor([9.0767e-06, 9.9999e-01, 1.5748e-07]))

这返回了三个东西:以与您最初提供的格式相同的预测类别(在本例中,这是一个字符串),预测类别的索引以及每个类别的概率。最后两个是基于DataLoadersvocab中类别的顺序;也就是说,所有可能类别的存储列表。在推理时,您可以将DataLoaders作为Learner的属性访问:

1
learn_inf.dls.vocab
1
(#3) ['black','grizzly','teddy']

我们可以看到,如果我们使用predict返回的整数索引到 vocab 中,我们会得到“灰熊”,这是预期的。另外,请注意,如果我们在概率列表中进行索引,我们会看到几乎有 1.00 的概率这是一只灰熊。

我们知道如何从保存的模型中进行预测,因此我们拥有开始构建我们的应用程序所需的一切。我们可以直接在 Jupyter 笔记本中完成。

从模型创建一个笔记本应用

要在应用程序中使用我们的模型,我们可以简单地将predict方法视为常规函数。因此,使用任何应用程序开发人员可用的各种框架和技术都可以创建一个从模型创建的应用程序。

然而,大多数数据科学家并不熟悉 Web 应用程序开发领域。因此,让我们尝试使用您目前已经了解的东西:事实证明,我们可以仅使用 Jupyter 笔记本创建一个完整的工作 Web 应用程序!使这一切成为可能的两个因素如下:

  • IPython 小部件(ipywidgets)

  • Voilà

IPython 小部件是 GUI 组件,它在 Web 浏览器中将 JavaScript 和 Python 功能结合在一起,并可以在 Jupyter 笔记本中创建和使用。例如,我们在本章前面看到的图像清理器完全是用 IPython 小部件编写的。但是,我们不希望要求我们的应用程序用户自己运行 Jupyter。

这就是Voilà存在的原因。它是一个使 IPython 小部件应用程序可供最终用户使用的系统,而无需他们使用 Jupyter。Voilà利用了一个事实,即笔记本已经是一种 Web 应用程序,只是另一个复杂的依赖于另一个 Web 应用程序:Jupyter 本身的 Web 应用程序。基本上,它帮助我们自动将我们已经隐式创建的复杂 Web 应用程序(笔记本)转换为一个更简单、更易部署的 Web 应用程序,它的功能类似于普通的 Web 应用程序,而不是笔记本。

但是我们仍然可以在笔记本中开发的优势,因此使用 ipywidgets,我们可以逐步构建我们的 GUI。我们将使用这种方法创建一个简单的图像分类器。首先,我们需要一个文件上传小部件:

1
2
btn_upload = widgets.FileUpload()
btn_upload

上传按钮

现在我们可以获取图像:

1
img = PILImage.create(btn_upload.data[-1])

表示图像的输出小部件

我们可以使用Output小部件来显示它:

1
2
3
4
out_pl = widgets.Output()
out_pl.clear_output()
with out_pl: display(img.to_thumb(128,128))
out_pl

表示图像的输出小部件

然后我们可以得到我们的预测:

1
pred,pred_idx,probs = learn_inf.predict(img)

并使用Label来显示它们:

1
2
3
lbl_pred = widgets.Label()
lbl_pred.value = f'Prediction: {pred}; Probability: {probs[pred_idx]:.04f}'
lbl_pred

预测:灰熊;概率:1.0000

我们需要一个按钮来进行分类。它看起来与上传按钮完全相同:

1
2
btn_run = widgets.Button(description='Classify')
btn_run

我们还需要一个点击事件处理程序;也就是说,当按下按钮时将调用的函数。我们可以简单地复制之前的代码行:

1
2
3
4
5
6
7
8
def on_click_classify(change):
img = PILImage.create(btn_upload.data[-1])
out_pl.clear_output()
with out_pl: display(img.to_thumb(128,128))
pred,pred_idx,probs = learn_inf.predict(img)
lbl_pred.value = f'Prediction: {pred}; Probability: {probs[pred_idx]:.04f}'

btn_run.on_click(on_click_classify)

您现在可以通过单击按钮来测试按钮,您应该会看到图像和预测会自动更新!

现在,我们可以将它们全部放在一个垂直框(VBox)中,以完成我们的 GUI:

1
2
VBox([widgets.Label('Select your bear!'),
btn_upload, btn_run, out_pl, lbl_pred])

整个小部件

我们已经编写了所有必要的应用程序代码。下一步是将其转换为我们可以部署的内容。

将您的笔记本变成一个真正的应用程序

现在我们在这个 Jupyter 笔记本中已经让一切运转起来了,我们可以创建我们的应用程序。为此,请启动一个新的笔记本,并仅添加创建和显示所需小部件的代码,以及任何要显示的文本的 Markdown。查看书中存储库中的bear_classifier笔记本,看看我们创建的简单笔记本应用程序。

接下来,如果您尚未安装 Voilà,请将这些行复制到笔记本单元格中并执行:

1
2
!pip install voila
!jupyter serverextension enable voila --sys-prefix

!开头的单元格不包含 Python 代码,而是包含传递给您的 shell(bash,Windows PowerShell 等)的代码。如果您习惯使用命令行,我们将在本书中更详细地讨论这一点,您当然可以直接在终端中键入这两行(不带!前缀)。在这种情况下,第一行安装voila库和应用程序,第二行将其连接到您现有的 Jupyter 笔记本。

Voilà运行 Jupyter 笔记本,就像您现在使用的 Jupyter 笔记本服务器一样,但它还做了一件非常重要的事情:它删除了所有单元格输入,仅显示输出(包括 ipywidgets),以及您的 Markdown 单元格。因此,剩下的是一个 Web 应用程序!要将您的笔记本视为 Voilà Web 应用程序,请将浏览器 URL 中的“notebooks”一词替换为“voila/render”。您将看到与您的笔记本相同的内容,但没有任何代码单元格。

当然,您不需要使用 Voilà或 ipywidgets。您的模型只是一个可以调用的函数(pred,pred_idx,probs = learn.predict(img)),因此您可以将其与任何框架一起使用,托管在任何平台上。您可以将在 ipywidgets 和 Voilà中原型设计的内容稍后转换为常规 Web 应用程序。我们在本书中展示这种方法,因为我们认为这是数据科学家和其他不是 Web 开发专家的人从其模型创建应用程序的绝佳方式。

我们有了我们的应用程序;现在让我们部署它!

部署您的应用程序

正如您现在所知,几乎任何有用的深度学习模型都需要 GPU 来训练。那么,在生产中使用该模型需要 GPU 吗?不需要!您几乎可以肯定在生产中不需要 GPU 来提供您的模型。这样做有几个原因:

  • 正如我们所见,GPU 仅在并行执行大量相同工作时才有用。如果您正在进行(比如)图像分类,通常一次只会对一个用户的图像进行分类,而且通常在一张图像中没有足够的工作量可以让 GPU 忙碌足够长的时间以使其非常有效。因此,CPU 通常更具成本效益。

  • 另一种选择可能是等待一些用户提交他们的图像,然后将它们批量处理并一次性在 GPU 上处理。但是这样会让用户等待,而不是立即得到答案!而且您需要一个高流量的网站才能实现这一点。如果您确实需要这种功能,您可以使用诸如 Microsoft 的ONNX RuntimeAWS SageMaker之类的工具。

  • 处理 GPU 推理的复杂性很大。特别是,GPU 的内存需要仔细手动管理,您需要一个仔细的排队系统,以确保一次只处理一个批次。

  • CPU 服务器的市场竞争要比 GPU 服务器更激烈,因此 CPU 服务器有更便宜的选项可供选择。

由于 GPU 服务的复杂性,许多系统已经出现尝试自动化此过程。然而,管理和运行这些系统也很复杂,通常需要将您的模型编译成专门针对该系统的不同形式。通常最好避免处理这种复杂性,直到/除非您的应用程序变得足够受欢迎,以至于您有明显的财务理由这样做。

至少对于您的应用程序的初始原型以及您想展示的任何爱好项目,您可以轻松免费托管它们。最佳位置和最佳方式随时间而变化,因此请查看本书网站以获取最新的建议。由于我们在 2020 年初撰写本书,最简单(且免费!)的方法是使用Binder。要在 Binder 上发布您的 Web 应用程序,请按照以下步骤操作:

  1. 将您的笔记本添加到GitHub 存储库

  2. 将该存储库的 URL 粘贴到 Binder 的 URL 字段中,如图 2-4 所示。

  3. 将文件下拉菜单更改为选择 URL。

  4. 在“要打开的 URL”字段中,输入/voila/render/*name*.ipynb(将name替换为您笔记本的名称)。

  5. 单击右下角的剪贴板按钮以复制 URL,并将其粘贴到安全位置。

  6. 单击“启动”。

部署到 Binder

图 2-4. 部署到 Binder

第一次执行此操作时,Binder 将花费大约 5 分钟来构建您的站点。在幕后,它正在查找一个可以运行您的应用程序的虚拟机,分配存储空间,并收集所需的文件以用于 Jupyter、您的笔记本以及将您的笔记本呈现为 Web 应用程序。

最后,一旦启动应用程序运行,它将导航您的浏览器到您的新 Web 应用程序。您可以分享您复制的 URL 以允许其他人访问您的应用程序。

要了解部署 Web 应用程序的其他(免费和付费)选项,请务必查看书籍网站

您可能希望将应用程序部署到移动设备或边缘设备,如树莓派。有许多库和框架允许您将模型直接集成到移动应用程序中。但是,这些方法往往需要许多额外的步骤和样板文件,并且并不总是支持您的模型可能使用的所有 PyTorch 和 fastai 层。此外,您所做的工作将取决于您针对部署的移动设备的类型 - 您可能需要做一些工作以在 iOS 设备上运行,不同的工作以在较新的 Android 设备上运行,不同的工作以在较旧的 Android 设备上运行,等等。相反,我们建议在可能的情况下,将模型本身部署到服务器,并让您的移动或边缘应用程序连接到它作为 Web 服务。

这种方法有很多优点。初始安装更容易,因为您只需部署一个小型 GUI 应用程序,该应用程序连接到服务器执行所有繁重的工作。更重要的是,核心逻辑的升级可以在您的服务器上进行,而不需要分发给所有用户。您的服务器将拥有比大多数边缘设备更多的内存和处理能力,并且如果您的模型变得更加苛刻,那么扩展这些资源将更容易。您在服务器上拥有的硬件也将更加标准化,并且更容易受到 fastai 和 PyTorch 的支持,因此您不必将模型编译成不同的形式。

当然也有缺点。你的应用程序将需要网络连接,每次调用模型时都会有一些延迟。(神经网络模型本来就需要一段时间来运行,所以这种额外的网络延迟在实践中可能对用户没有太大影响。事实上,由于你可以在服务器上使用更好的硬件,总体延迟甚至可能比在本地运行时更少!)此外,如果你的应用程序使用敏感数据,你的用户可能会担心采用将数据发送到远程服务器的方法,因此有时隐私考虑将意味着你需要在边缘设备上运行模型(通过在公司防火墙内部设置本地服务器可能可以避免这种情况)。管理复杂性和扩展服务器也可能会带来额外的开销,而如果你的模型在边缘设备上运行,每个用户都会带来自己的计算资源,这将导致随着用户数量的增加更容易扩展(也称为水平扩展)。

Alexis 说

我有机会近距离看到移动机器学习领域在我的工作中是如何变化的。我们提供一个依赖于计算机视觉的 iPhone 应用程序,多年来我们在云中运行我们自己的计算机视觉模型。那时这是唯一的方法,因为那些模型需要大量的内存和计算资源,并且需要几分钟来处理输入。这种方法不仅需要构建模型(有趣!),还需要构建基础设施来确保一定数量的“计算工作机器”始终在运行(可怕),如果流量增加,更多的机器会自动上线,有稳定的存储用于大型输入和输出,iOS 应用程序可以知道并告诉用户他们的工作进展如何等等。如今,苹果提供了 API,可以将模型转换为在设备上高效运行,大多数 iOS 设备都有专用的 ML 硬件,所以这是我们用于新模型的策略。这仍然不容易,但在我们的情况下,为了更快的用户体验和更少地担心服务器,这是值得的。对你来说有效的方法将取决于你试图创建的用户体验以及你个人认为容易做的事情。如果你真的知道如何运行服务器,那就去做。如果你真的知道如何构建本地移动应用程序,那就去做。有很多条路通往山顶。

总的来说,我们建议在可能的情况下尽可能使用简单的基于 CPU 的服务器方法,只要你能够做到。如果你足够幸运拥有一个非常成功的应用程序,那么你将能够在那个时候为更复杂的部署方法进行投资。

恭喜你——你已经成功构建了一个深度学习模型并部署了它!现在是一个很好的时机停下来思考可能出现的问题。

如何避免灾难

在实践中,一个深度学习模型只是一个更大系统中的一部分。正如我们在本章开头讨论的那样,构建数据产品需要考虑整个端到端的过程,从概念到在生产中使用。在这本书中,我们无法希望涵盖所有管理部署数据产品的复杂性,比如管理多个模型版本,A/B 测试,金丝雀发布,刷新数据(我们应该一直增加和增加我们的数据集,还是应该定期删除一些旧数据?),处理数据标记,监控所有这些,检测模型腐烂等等。

在本节中,我们将概述一些需要考虑的最重要问题;关于部署问题的更详细讨论,我们建议您参考 Emmanuel Ameisin(O’Reilly)的优秀著作《构建机器学习驱动的应用程序》。

需要考虑的最大问题之一是,理解和测试深度学习模型的行为比大多数其他代码更困难。在正常软件开发中,您可以分析软件所采取的确切步骤,并仔细研究这些步骤中哪些与您试图创建的期望行为相匹配。但是,对于神经网络,行为是从模型尝试匹配训练数据中产生的,而不是精确定义的。

这可能导致灾难!例如,假设我们真的正在推出一个熊检测系统,将连接到国家公园露营地周围的视频摄像头,并警告露营者有熊靠近。如果我们使用下载的数据集训练的模型,实际上会出现各种问题,比如:

  • 处理视频数据而不是图像

  • 处理可能不在数据集中出现的夜间图像

  • 处理低分辨率摄像头图像

  • 确保结果返回得足够快以在实践中有用

  • 在照片中很少见到的位置识别熊(例如从背后,部分被灌木覆盖,或者离摄像机很远)

问题的一个重要部分是,人们最有可能上传到互联网的照片是那些能够清晰艺术地展示主题的照片,而这并不是该系统将获得的输入类型。因此,我们可能需要进行大量自己的数据收集和标记以创建一个有用的系统。

这只是更一般的“域外”数据问题的一个例子。也就是说,在生产中,我们的模型可能看到与训练时非常不同的数据。这个问题没有完全的技术解决方案;相反,我们必须谨慎地推出技术。

我们还需要小心的其他原因。一个非常常见的问题是域漂移,即我们的模型看到的数据类型随着时间的推移而发生变化。例如,一个保险公司可能将深度学习模型用作其定价和风险算法的一部分,但随着时间的推移,公司吸引的客户类型和代表的风险类型可能发生如此大的变化,以至于原始训练数据不再相关。

域外数据和域漂移是更大问题的例子:您永远无法完全理解神经网络的所有可能行为,因为它们有太多参数。这是它们最好特性的自然缺点——它们的灵活性,使它们能够解决我们甚至可能无法完全指定首选解决方案的复杂问题。然而,好消息是,有办法通过一个经过深思熟虑的过程来减轻这些风险。这些细节将根据您正在解决的问题的细节而变化,但我们将尝试提出一个高层次的方法,总结在图 2-5 中,我们希望这将提供有用的指导。

部署过程

杰里米说

20 年前,我创办了一家名为 Optimal Decisions 的公司,利用机器学习和优化帮助巨大的保险公司设定价格,影响数千亿美元的风险。我们使用这里描述的方法来管理可能出错的潜在风险。此外,在与客户合作将任何东西投入生产之前,我们尝试通过在他们去年的数据上测试端到端系统的影响来模拟影响。将这些新算法投入生产总是一个非常紧张的过程,但每次推出都取得了成功。

意想不到的后果和反馈循环

推出模型的最大挑战之一是,您的模型可能会改变其所属系统的行为。例如,考虑一个“预测执法”算法,它预测某些社区的犯罪率更高,导致更多警察被派往这些社区,这可能导致这些社区记录更多犯罪,依此类推。在皇家统计学会的论文“预测和服务?”中,Kristian Lum 和 William Isaac 观察到“预测性执法的命名恰如其分:它预测未来的执法,而不是未来的犯罪。”

在这种情况下的部分问题是,在存在偏见的情况下(我们将在下一章中深入讨论),反馈循环可能导致该偏见的负面影响变得越来越严重。例如,在美国已经存在着在种族基础上逮捕率存在显著偏见的担忧。根据美国公民自由联盟的说法,“尽管使用率大致相等,黑人因大麻被逮捕的可能性是白人的 3.73 倍。”这种偏见的影响,以及在美国许多地区推出预测性执法算法,导致 Bärí Williams 在纽约时报中写道:“在我的职业生涯中引起如此多兴奋的技术正在以可能意味着在未来几年,我的 7 岁儿子更有可能因为他的种族和我们居住的地方而被无故定性或逮捕,甚至更糟。”

在推出重要的机器学习系统之前,一个有用的练习是考虑这个问题:“如果它真的很成功会发生什么?”换句话说,如果预测能力非常高,对行为的影响非常显著,那么会发生什么?谁会受到最大影响?最极端的结果可能是什么样的?你怎么知道到底发生了什么?

这样的思考练习可能会帮助你制定一个更加谨慎的推出计划,配备持续监控系统和人类监督。当然,如果人类监督没有被听取,那么它就没有用,因此确保可靠和有弹性的沟通渠道存在,以便正确的人会意识到问题并有权力解决它们。

课后练习

  1. 文本模型目前存在哪些主要不足之处?
  • 深度学习不擅长生成正确的回应!
  1. 文本生成模型可能存在哪些负面社会影响?
  • 用来传播虚假消息,鼓动不安情绪,或者是骗你钱骗你感情哈哈哈哈(因为现在你快已经无法分辨出你在和人类聊天还是深度学习机器人)
  1. 在模型可能犯错且这些错误可能有害的情况下,自动化流程的一个好的替代方案是什么?
  • 由于这个严重问题,我们通常建议深度学习不要作为完全自动化的过程,而是作为模型和人类用户密切互动的过程的一部分。这可能使人类的生产力比完全手动方法高出几个数量级,并且比仅使用人类更准确。深度学习模型现在仅仅是人类可以用来参考的工具而非你问就给你答案的神灯
  1. 深度学习在哪种表格数据上特别擅长?
  • 包含自然语言(书名、评论等)和高基数分类列(即包含大量离散选择的内容,如邮政编码或产品 ID)。关于随机森林或梯度提升的表格(数据类型)就需要花更长的时间训练了。
  1. 直接使用深度学习模型进行推荐系统的一个主要缺点是什么?
  • 他会推荐对你无用的产品或者你已经知道的东西,比如你已经买了某个东西他还是会给你推荐。
  1. 驱动器方法的步骤是什么?
  • 定义一个明确的目标
  • 可以采取的行动
  • 需要什么新数据来生成这样的排名
  • 构建预测模型
  • 构建多个模型,比较差异
  1. 驱动器方法的步骤如何映射到推荐系统?
  • 推荐引擎的目标是通过推荐客户不会在没有推荐的情况下购买的物品来推动额外的销售。杠杆是推荐的排名。必须收集新数据以生成将导致新销售的推荐。这将需要进行许多随机实验,以收集关于各种客户的各种推荐的数据。这是很少有组织采取的一步;但是没有它,您就没有所需的信息来根据您的真正目标(更多销售!)优化推荐。

  • 最后,您可以为购买概率构建两个模型,条件是看到或没有看到推荐。这两个概率之间的差异是给定推荐给客户的效用函数。在算法推荐客户已经拒绝的熟悉书籍(两个组成部分都很小)或者他们本来就会购买的书籍(两个组成部分都很大并互相抵消)的情况下,效用函数会很低。

  1. 使用你策划的数据创建一个图像识别模型,并将其部署在网络上。(还未成功因为获取数据集api的问题)

  2. DataLoaders是什么?

  • 一个 fastai 类,存储您传递给它的多个DataLoader对象——通常是一个train和一个valid,尽管可以有任意数量。前两个作为属性提供。
  1. 我们需要告诉 fastai 创建DataLoaders的四件事是什么?
  • 我们正在处理什么类型的数据

  • 如何获取项目列表

  • 如何为这些项目打标签

  • 如何创建验证集

  1. DataBlock中的splitter参数是做什么的?
  • 用来在数据集中分出训练集和验证集(设定验证集比率valid_pct和起始点seed)
  1. 我们如何确保随机分割总是给出相同的验证集?
  • 保证比率valid_pct和起始点seed一致即可
  1. 哪些字母通常用来表示自变量和因变量?
  • 自变量通常被称为x,因变量通常被称为y。在这里,我们告诉 fastai 要调用哪个函数来创建数据集中的标签:
1
get_y=parent_label
  1. 裁剪、填充和压缩调整方法之间有什么区别?在什么情况下你会选择其中之一?
  • 如果我们压缩或拉伸图像,它们最终会变成不现实的形状,导致模型学习到事物看起来与实际情况不同,这会导致更低的准确性。

  • 如果我们裁剪图像,我们会移除一些允许我们进行识别的特征。例如,如果我们试图识别狗或猫的品种,我们可能会裁剪掉区分相似品种所需的身体或面部的关键部分。

  • 如果我们填充图像,就会有很多空白空间,这对我们的模型来说只是浪费计算,并导致我们实际使用的图像部分具有较低的有效分辨率。

  1. 什么是数据增强?为什么需要它?
  • 数据增强指的是创建输入数据的随机变化,使它们看起来不同但不改变数据的含义。对于图像的常见数据增强技术包括旋转、翻转、透视变形、亮度变化和对比度变化。

  • 一个完全未经训练的神经网络对图像的行为一无所知。它甚至不认识当一个物体旋转一度时,它仍然是同一物体的图片!因此,通过训练神经网络使用物体在略有不同位置并且大小略有不同的图像的示例,有助于它理解物体的基本概念,以及如何在图像中表示它。
    所以需要图像增强进行训练

  1. 提供一个例子,说明熊分类模型在生产中可能因训练数据的结构或风格差异而效果不佳。
  • 文中例子
  1. item_tfmsbatch_tfms之间有什么区别?
  • 图像现在都是相同大小,我们可以使用 GPU 将这些增强应用于整个批次的图像,这将节省大量时间。要告诉 fastai 我们要在批次上使用这些变换,我们使用batch_tfms参数(请注意,在此示例中我们没有使用RandomResizedCrop,这样您可以更清楚地看到差异;出于同样的原因,我们使用了默认值的两倍的增强量)

  • 我们需要添加一个转换,将这些图像调整为相同的大小。Item transforms是在每个单独项目上运行的代码片段,无论是图像、类别还是其他

  1. 混淆矩阵是什么?
  • 行代表数据集中所有黑色、灰熊和泰迪熊,列分别代表模型预测为黑色、灰熊和泰迪熊的图像。因此,矩阵的对角线显示了被正确分类的图像,而非对角线的单元格代表被错误分类的图像。这是 fastai 允许您查看模型结果的许多方式之一。当然,这是使用验证集计算的。通过颜色编码,目标是在对角线以外的地方都是白色,而在对角线上我们希望是深蓝色。我们的熊分类器几乎没有犯错!

看到我们的错误发生在哪里是有帮助的,以便确定它们是由数据集问题(例如,根本不是熊的图像,或者标记错误)还是模型问题(也许它无法处理使用不同光照或从不同角度拍摄的图像等)。为了做到这一点,我们可以根据损失对图像进行排序。

损失是一个数字,如果模型不正确(尤其是如果它对其不正确的答案也很自信),或者如果它是正确的但对其正确答案不自信,那么损失就会更高。在第二部分的开头,我们将深入学习损失是如何计算和在训练过程中使用的。现在,plot_top_losses向我们展示了数据集中损失最高的图像。正如输出的标题所说,每个图像都标有四个内容:预测、实际(目标标签)、损失和概率。这里的概率是模型对其预测分配的置信水平,从零到一:

1
2
interp = ClassificationInterpretation.from_learner(learn)
interp.plot_confusion_matrix()

  1. export保存了什么?
  • 当您调用export时,fastai 将保存一个名为export.pkl的文件其中包含架构和参数
  1. 当我们使用模型进行预测而不是训练时,这被称为什么?
  • 当我们使用模型进行预测而不是训练时,我们称之为推理
  1. IPython 小部件是什么?
  • IPython 小部件是 GUI 组件,它在 Web 浏览器中将 JavaScript 和 Python 功能结合在一起,并可以在 Jupyter 笔记本中创建和使用。例如,我们在本章前面看到的图像清理器完全是用 IPython 小部件编写的。但是,我们不希望要求我们的应用程序用户自己运行 Jupyter。
  1. 什么时候会使用 CPU 进行部署?什么时候 GPU 可能更好?
  • 几乎任何有用的深度学习模型都需要 GPU 来训练。那么,在生产中使用该模型需要 GPU 吗?不需要!您几乎可以肯定在生产中不需要 GPU 来提供您的模型。这样做有几个原因:

  • 正如我们所见,GPU 仅在并行执行大量相同工作时才有用。如果您正在进行(比如)图像分类,通常一次只会对一个用户的图像进行分类,而且通常在一张图像中没有足够的工作量可以让 GPU 忙碌足够长的时间以使其非常有效。因此,CPU 通常更具成本效益。

  • 另一种选择可能是等待一些用户提交他们的图像,然后将它们批量处理并一次性在 GPU 上处理。但是这样会让用户等待,而不是立即得到答案!而且您需要一个高流量的网站才能实现这一点。如果您确实需要这种功能,您可以使用诸如 Microsoft 的ONNX RuntimeAWS SageMaker之类的工具。

  • 处理 GPU 推理的复杂性很大。特别是,GPU 的内存需要仔细手动管理,您需要一个仔细的排队系统,以确保一次只处理一个批次。

  • CPU 服务器的市场竞争要比 GPU 服务器更激烈,因此 CPU 服务器有更便宜的选项可供选择。

  1. 将应用部署到服务器而不是客户端(或边缘)设备(如手机或 PC)的缺点是什么?
  • 你的应用程序将需要网络连接,每次调用模型时都会有一些延迟。
  1. 在实践中推出熊警告系统时可能出现的三个问题的例子是什么?
  • 处理视频数据而不是图像

  • 处理可能不在数据集中出现的夜间图像

  • 处理低分辨率摄像头图像

  • 确保结果返回得足够快以在实践中有用

  • 在照片中很少见到的位置识别熊(例如从背后,部分被灌木覆盖,或者离摄像机很远)

  1. 什么是域外数据?
  • 人们最有可能上传到互联网的照片是那些能够清晰艺术地展示主题的照片,而这并不是该系统将获得的输入类型。因此,我们可能需要进行大量自己的数据收集和标记以创建一个有用的系统。

  • 这只是更一般的“域外”数据问题的一个例子。也就是说,在生产中,我们的模型可能看到与训练时非常不同的数据。这个问题没有完全的技术解决方案;相反,我们必须谨慎地推出技术。

  1. 什么是领域转移?
  • 即我们的模型看到的数据类型随着时间的推移而发生变化。例如,一个保险公司可能将深度学习模型用作其定价和风险算法的一部分,但随着时间的推移,公司吸引的客户类型和代表的风险类型可能发生如此大的变化,以至于原始训练数据不再相关。
  1. 部署过程中的三个步骤是什么?
  • 第一步是使用完全手动的过程

  • 第二步是尝试限制模型的范围

  • 逐渐扩大您的推出范围

进一步研究

  1. 考虑一下驱动器方法如何映射到你感兴趣的项目或问题。

  2. 在什么情况下最好避免某些类型的数据增强?

  3. 对于你有兴趣应用深度学习的项目,考虑一下这个思维实验,“如果它进展得非常顺利会发生什么?”

  4. 开始写博客,撰写你的第一篇博客文章。例如,写一下你认为深度学习在你感兴趣的领域可能有用的地方。

什么是机器学习?

你的分类器是一个深度学习模型。正如已经提到的,深度学习模型使用神经网络,这些神经网络最初可以追溯到上世纪 50 年代,并且最近由于最新的进展变得非常强大。

另一个重要的背景是,深度学习只是更一般的机器学习领域中的一个现代领域。要理解当你训练自己的分类模型时所做的事情的本质,你不需要理解深度学习。看到你的模型和训练过程是如何成为适用于机器学习的概念的例子就足够了。

因此,在本节中,我们将描述机器学习。我们将探讨关键概念,并看看它们如何可以追溯到最初介绍它们的原始文章。

机器学习就像常规编程一样,是让计算机完成特定任务的一种方式。但是如果要用常规编程来完成前面部分我们刚刚做的事情:在照片中识别狗和猫,我们将不得不为计算机写下完成任务所需的确切步骤。

通常,当我们编写程序时,很容易为我们写下完成任务的步骤。我们只需考虑如果我们必须手动完成任务时会采取的步骤,然后将它们转换为代码。例如,我们可以编写一个对列表进行排序的函数。一般来说,我们会编写一个类似于图 1-4 的函数(其中inputs可能是一个未排序的列表,results是一个排序后的列表)。

管道输入、程序、结果

图 1-4. 传统程序

但是要在照片中识别物体,这有点棘手;当我们在图片中识别物体时,我们采取了什么步骤?我们真的不知道,因为这一切都发生在我们的大脑中,而我们并没有意识到!

早在计算机诞生之初,1949 年,IBM 的一位研究员阿瑟·塞缪尔开始研究一种让计算机完成任务的不同方式,他称之为机器学习。在他经典的 1962 年文章“人工智能:自动化的前沿”中,他写道:

为这样的计算编程对于我们来说是相当困难的,主要不是因为计算机本身的任何固有复杂性,而是因为需要详细说明过程的每一个细微步骤。任何程序员都会告诉你,计算机是巨大的白痴,而不是巨大的大脑。

他的基本想法是这样的:不是告诉计算机解决问题所需的确切步骤,而是向其展示解决问题的示例,并让它自己找出如何解决。结果证明这非常有效:到 1961 年,他的跳棋程序学到了很多,以至于击败了康涅狄格州冠军!这是他描述自己想法的方式(与之前提到的同一篇文章):

假设我们安排一些自动手段来测试任何当前权重分配的有效性,以实际表现为准,并提供一种机制来改变权重分配以最大化性能。我们不需要详细了解这种程序的细节,就可以看到它可以完全自动化,并且可以看到一个这样编程的机器将从中学习。

这个简短陈述中嵌入了一些强大的概念:

  • “权重分配”的想法

  • 每个权重分配都有一些“实际表现”的事实

  • 要求有一种“自动手段”来测试该性能

  • 需要一个“机制”(即,另一个自动过程)来通过改变权重分配来提高性能

让我们逐一了解这些概念,以便了解它们在实践中如何结合。首先,我们需要了解塞缪尔所说的权重分配是什么意思。

权重只是变量,权重分配是这些变量的特定值选择。程序的输入是它处理以产生结果的值,例如,将图像像素作为输入,并返回分类“狗”作为结果。程序的权重分配是定义程序操作方式的其他值。

因为它们会影响程序,它们在某种意义上是另一种输入。我们将更新我们的基本图片图 1-4,并用图 1-5 替换,以便考虑到这一点。

图 1-5。使用权重分配的程序

我们已将方框的名称从程序更改为模型。这是为了遵循现代术语并反映模型是一种特殊类型的程序:它可以根据权重许多不同的事情。它可以以许多不同的方式实现。例如,在塞缪尔的跳棋程序中,不同的权重值会导致不同的跳棋策略。

(顺便说一句,塞缪尔所说的“权重”如今通常被称为模型参数,以防您遇到这个术语。术语权重保留给特定类型的模型参数。)

接下来,塞缪尔说我们需要一种自动测试任何当前权重分配的有效性的方法,以实际表现为准。在他的跳棋程序中,“实际表现”模型的表现有多好。您可以通过让两个模型相互对战并看哪个通常获胜来自动测试两个模型的表现。

最后,他说我们需要一种机制来改变权重分配,以最大化性能。例如,我们可以查看获胜模型和失败模型之间的权重差异,并将权重进一步调整到获胜方向。

我们现在可以看到他为什么说这样的程序可以完全自动化,并且…一个这样编程的机器将从中学习。当权重的调整也是自动的时,学习将变得完全自动——当我们不再通过手动调整权重来改进模型,而是依赖于根据性能产生调整的自动化机制时。

图 1-6 展示了塞缪尔关于训练机器学习模型的完整图景。

!基本训练循环

图 1-6。训练机器学习模型

注意模型的结果(例如,在跳棋游戏中的移动)和其性能(例如,是否赢得比赛,或者赢得比赛的速度)之间的区别。

还要注意,一旦模型训练好了,也就是说,一旦我们选择了最终的、最好的、最喜欢的权重分配,那么我们可以将权重视为模型的一部分,因为我们不再对它们进行变化。

因此,实际上在训练后使用模型看起来像图 1-7。

图 1-7。使用训练后的模型作为程序

这看起来与我们在图 1-4 中的原始图表相同,只是将程序一词替换为模型。这是一个重要的观点:训练后的模型可以像常规计算机程序一样对待

行话:机器学习

通过让计算机从经验中学习而不是通过手动编码个别步骤来开发程序的培训。

什么是神经网络?

不难想象跳棋程序的模型可能是什么样子。可能编码了一系列跳棋策略,以及某种搜索机制,然后权重可以变化以决定如何选择策略,在搜索期间关注棋盘的哪些部分等等。但是对于图像识别程序,或者理解文本,或者我们可能想象的许多其他有趣的问题,模型可能是什么样子却一点也不明显。

我们希望有一种函数,它如此灵活,以至于可以通过调整其权重来解决任何给定问题。令人惊讶的是,这种函数实际上存在!这就是我们已经讨论过的神经网络。也就是说,如果您将神经网络视为数学函数,那么它将是一种极其灵活的函数,取决于其权重。一种称为通用逼近定理的数学证明表明,这种函数在理论上可以解决任何问题,达到任何精度水平。神经网络如此灵活的事实意味着,在实践中,它们通常是一种合适的模型,您可以将精力集中在训练过程上,即找到良好的权重分配。

但是这个过程呢?人们可以想象,您可能需要为每个问题找到一种新的“机制”来自动更新权重。这将是费力的。我们在这里也希望有一种完全通用的方法来更新神经网络的权重,使其在任何给定任务上都能提高。方便的是,这也存在!

这被称为随机梯度下降(SGD)。我们将在第四章中详细了解神经网络和 SGD 的工作原理,以及解释通用逼近定理。然而,现在,我们将使用塞缪尔自己的话来说:我们不需要深入了解这样一个过程的细节,就可以看到它可以完全自动化,并且可以看到这样一个机器编程的机器可以从中学习经验。

杰里米说

不要担心;无论是 SGD 还是神经网络,在数学上都不复杂。它们几乎完全依赖于加法和乘法来完成工作(但它们进行了大量的加法和乘法!)。当学生们看到细节时,我们听到的主要反应是:“就是这样吗?”

换句话说,简而言之,神经网络是一种特殊类型的机器学习模型,它完全符合塞缪尔最初的构想。神经网络之所以特殊,是因为它们非常灵活,这意味着它们可以通过找到正确的权重来解决异常广泛的问题。这是强大的,因为随机梯度下降为我们提供了一种自动找到这些权重值的方法。

放大后,让我们现在缩小范围,重新审视使用塞缪尔框架解决我们的图像分类问题。

我们的输入是图像。我们的权重是神经网络中的权重。我们的模型是一个神经网络。我们的结果是由神经网络计算出的值,比如“狗”或“猫”。

下一个部分是什么,一个自动测试任何当前权重分配的有效性的手段?确定“实际表现”很容易:我们可以简单地将模型的表现定义为其在预测正确答案时的准确性。

将所有这些放在一起,假设 SGD 是我们更新权重分配的机制,我们可以看到我们的图像分类器是一个机器学习模型,就像 Samuel 所设想的那样。

一些深度学习术语

Samuel 在 1960 年代工作,自那时术语已经发生了变化。以下是我们讨论过的所有部分的现代深度学习术语:

  • 模型的功能形式被称为架构(但要小心—有时人们将模型用作架构的同义词,这可能会让人困惑)。

  • 权重被称为参数

  • 预测是从独立变量计算出来的,这是不包括标签数据

  • 模型的结果被称为预测

  • 性能的度量被称为损失

  • 损失不仅取决于预测,还取决于正确的标签(也称为目标因变量);例如,“狗”或“猫”。

在进行这些更改后,我们在图 1-6 中的图表看起来像图 1-8。

图 1-8. 详细训练循环

机器学习固有的限制

从这幅图片中,我们现在可以看到关于训练深度学习模型的一些基本事情:

  • 没有数据就无法创建模型。

  • 模型只能学习操作训练数据中看到的模式。

  • 这种学习方法只创建预测,而不是推荐的行动

  • 仅仅拥有输入数据的示例是不够的;我们还需要为这些数据提供标签(例如,仅有狗和猫的图片不足以训练模型;我们需要为每个图片提供一个标签,说明哪些是狗,哪些是猫)。

一般来说,我们已经看到大多数组织声称他们没有足够的数据实际上意味着他们没有足够的带标签数据。如果任何组织有兴趣在实践中使用模型做一些事情,那么他们可能有一些输入数据计划运行他们的模型。并且可能他们已经以其他方式做了一段时间(例如,手动或使用一些启发式程序),因此他们有来自这些过程的数据!例如,放射学实践几乎肯定会有医学扫描的存档(因为他们需要能够检查他们的患者随时间的进展),但这些扫描可能没有包含诊断或干预措施列表的结构化标签(因为放射科医生通常创建自由文本自然语言报告,而不是结构化数据)。在本书中,我们将大量讨论标记方法,因为这在实践中是一个非常重要的问题。

由于这类机器学习模型只能进行预测(即试图复制标签),这可能导致组织目标与模型能力之间存在显著差距。例如,在本书中,您将学习如何创建一个推荐系统,可以预测用户可能购买的产品。这通常用于电子商务,例如通过显示排名最高的商品来定制主页上显示的产品。但这样的模型通常是通过查看用户及其购买历史(输入)以及他们最终购买或查看的内容(标签)来创建的,这意味着该模型很可能会告诉您关于用户已经拥有或已经了解的产品,而不是他们最有可能对其感兴趣的新产品。这与您当地书店的专家所做的事情大不相同,他们会询问您的口味,然后告诉您您以前从未听说过的作者或系列。

另一个关键的洞察来自于考虑模型如何与其环境互动。这可能会产生反馈循环,如此处所述:

  1. 基于过去的逮捕地点创建了一个预测性执法模型。实际上,这并不是在预测犯罪,而是在预测逮捕,因此部分地只是反映了现有执法过程中的偏见。

  2. 然后执法人员可能会使用该模型来决定在哪里集中他们的执法活动,导致这些地区的逮捕增加。

  3. 这些额外逮捕的数据将被反馈回去重新训练未来版本的模型。

这是一个正反馈循环:模型被使用得越多,数据就变得越有偏见,使模型变得更加有偏见,依此类推。

反馈循环也可能在商业环境中造成问题。例如,视频推荐系统可能会偏向于推荐由视频最大观看者消费的内容(例如,阴谋论者和极端分子倾向于观看比平均水平更多的在线视频内容),导致这些用户增加他们的视频消费量,进而导致更多这类视频被推荐。我们将在第三章中更详细地讨论这个话题。

既然你已经看到了理论的基础,让我们回到我们的代码示例,详细看看代码如何与我们刚刚描述的过程相对应。

代码如何工作

让我们看看我们的图像识别器代码如何映射到这些想法。我们将把每一行放入一个单独的单元格,并查看每一行正在做什么(我们暂时不会解释每个参数的每个细节,但会给出重要部分的描述;完整细节将在本书后面提供)。第一行导入了整个 fastai.vision 库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from fastai.vision.all import * 
#第一行导入了整个 fastai.vision 库,*代表导入所有的函数类变量,这通常在常规编程中不出现因为会造成很多的错误。
path = untar_data(URLs.PETS)/'images'
#第二行从[fast.ai 数据集合](https://course.fast.ai/datasets)下载一个标准数据集(如果之前没有下载),将其提取出来(如果之前没有提取),并返回一个提取位置的`Path`对象。
def is_cat(x): return x[0].isupper()
#定义了一个函数`is_cat`,根据数据集创建者提供的文件名规则来标记猫,猫狗利用文件名大小写区分。
dls = ImageDataLoaders.from_name_func(
path, get_image_files(path), valid_pct=0.2, seed=42,
label_func=is_cat, item_tfms=Resize(224))
#第四行使用了这个函数,告诉 fastai 我们拥有什么类型的数据集以及它的结构
#调用`from_name_func`来实现(这意味着可以使用应用于文件名的函数来提取文件名),并传递`x[0].isupper()`,如果第一个字母是大写字母(即是猫),则评估为`True`。
#`valid_pct=0.2`。这告诉 fastai 保留 20%的数据,*完全不用于训练模型*。这 20%的数据被称为*验证集*;剩下的 80%被称为*训练集*。验证集用于衡量模型的准确性。
#参数`seed=42`将*随机种子*设置为每次运行此代码时相同的值,这意味着每次运行时我们都会得到相同的验证集,这样,如果我们更改模型并重新训练它,我们知道任何差异都是由于对模型的更改,而不是由于有不同的随机验证集。
learn = cnn_learner(dls, resnet34, metrics=error_rate)
#第五行告诉 fastai 创建一个*卷积神经网络*(CNN),并指定要使用的*架构*(即要创建的模型类型)、我们要对其进行训练的数据以及要使用的*度量标准*。
#`cnn_learner`还有一个名为`pretrained`的参数,默认值为`True`(因此在这种情况下使用,即使我们没有指定),它将您模型中的权重设置为已经由专家训练过的值,以识别 130 万张照片中的一千个不同类别(使用著名的[*ImageNet*](http://www.image-net.org)数据集),即使用预训练模型。
learn.fine_tune(1)
#第六行告诉 fastai 如何适应模型,自动使用微调技术。

不同类型的深度学习数据集和问题有各种类别,这里我们使用ImageDataLoaders。类名的第一部分通常是你拥有的数据类型,比如图像或文本。

我们必须告诉 fastai 的另一个重要信息是如何从数据集中获取标签。计算机视觉数据集通常以标签作为文件名或路径的一部分进行结构化,最常见的是父文件夹名称。fastai 带有许多标准化的标记方法,以及编写自己的方法。在这里,我们告诉 fastai 使用我们刚刚定义的is_cat函数。

最后,我们定义了我们需要的TransformTransform包含在训练期间自动应用的代码;fastai 包含许多预定义的Transform,添加新的Transform就像创建一个 Python 函数一样简单。有两种类型:item_tfms应用于每个项目(在本例中,每个项目都被调整为 224 像素的正方形),而batch_tfms应用于一次处理一批项目的 GPU,因此它们特别快速(我们将在本书中看到许多这样的例子)。

为什么是 224 像素?出于历史原因(旧的预训练模型需要这个确切的尺寸),但你几乎可以传入任何尺寸。如果增加尺寸,通常会得到更好的模型结果(因为它可以关注更多细节),但代价是速度和内存消耗;如果减小尺寸,则相反。

为什么使用 CNN?这是创建计算机视觉模型的当前最先进方法。我们将在本书中学习有关 CNN 如何工作的所有知识。它们的结构受到人类视觉系统工作方式的启发。

在 fastai 中有许多架构,我们将在本书中介绍(以及讨论如何创建您自己的架构)。然而,大多数情况下,选择架构并不是深度学习过程中非常重要的部分。这是学术界喜欢谈论的内容,但实际上您不太可能需要花费太多时间。有一些标准架构在大多数情况下都有效,而在这种情况下,我们使用的是一种称为ResNet的架构,我们将在本书中大量讨论;它对许多数据集和问题都既快速又准确。resnet34中的34指的是该架构变体中的层数(其他选项是1850101152)。使用层数更多的架构模型训练时间更长,更容易过拟合(即在验证集上的准确率开始变差之前无法训练多少个时期)。另一方面,当使用更多数据时,它们可能会更准确。

什么是度量标准?度量标准是一个函数,使用验证集来衡量模型预测的质量,并将在每个时期结束时打印出来。在这种情况下,我们使用error_rate,这是 fastai 提供的一个函数,它正是它所说的:告诉您验证集中有多少百分比的图像被错误分类。分类的另一个常见度量标准是accuracy(即1.0 - error_rate)。fastai 提供了许多其他度量标准,这将在本书中讨论。

度量标准的概念可能会让您想起损失,但有一个重要区别。损失的整个目的是定义一个“性能度量”,训练系统可以使用它来自动更新权重。换句话说,损失的一个好选择是易于随机梯度下降使用的选择。但度量标准是为人类消费而定义的,因此一个好的度量标准是您易于理解的,并且尽可能接近您希望模型执行的任务。有时,您可能会决定损失函数是一个合适的度量标准,但这并不一定是情况。

使用预训练模型时,cnn_learner将移除最后一层,因为该层始终是针对原始训练任务(即 ImageNet 数据集分类)专门定制的,并将其替换为一个或多个具有随机权重的新层,适合您正在处理的数据集的大小。模型的最后部分被称为

这是深度学习的关键之处——确定如何适应模型的参数以使其解决您的问题。要适应一个模型,我们必须提供至少一条信息:每个图像查看多少次(称为时代数)。您选择的时代数将在很大程度上取决于您有多少时间可用,以及您发现在实践中适应模型需要多长时间。如果选择的数字太小,您可以随时稍后进行更多时代的训练。

但为什么这种方法被称为fine_tune,而不是fit?fastai 确实有一个名为fit的方法,它确实适合一个模型(即,多次查看训练集中的图像,每次更新参数使预测越来越接近目标标签)。但在这种情况下,我们已经从一个预训练模型开始,并且我们不想丢弃它已经具有的所有这些功能。正如您将在本书中了解到的,有一些重要的技巧可以使预训练模型适应新数据集,这个过程称为微调。

微调一种迁移学习技术,通过使用与预训练不同的任务进行额外时代的训练来更新预训练模型的参数。

当您使用fine_tune方法时,fastai 将为您使用这些技巧。您可以设置一些参数(我们稍后会讨论),但在此处显示的默认形式中,它执行两个步骤:

  1. 使用一个时代来适应模型的那些部分,以使新的随机头部能够正确地与您的数据集配合工作。

  2. 在调用适合整个模型的方法时,请使用请求的时代数,更快地更新后面的层(特别是头部)的权重,而不是早期的层(正如我们将看到的,通常不需要对预训练权重进行太多更改)。

模型的头部是新添加的部分,专门针对新数据集。一个时代是对数据集的一次完整遍历。在调用fit之后,每个时代后的结果都会被打印出来,显示时代编号,训练和验证集的损失(用于训练模型的“性能度量”),以及您请求的任何指标(在这种情况下是错误率)。

因此,通过所有这些代码,我们的模型学会了仅仅通过标记的示例来识别猫和狗。但它是如何做到的呢?

我们的图像识别器学到了什么

在这个阶段,我们有一个工作良好的图像识别器,但我们不知道它在做什么!尽管许多人抱怨深度学习导致不可理解的“黑匣子”模型(即,可以提供预测但没有人能理解的东西),但事实并非如此。有大量研究表明如何深入检查深度学习模型并从中获得丰富的见解。话虽如此,各种机器学习模型(包括深度学习和传统统计模型)都可能难以完全理解,特别是考虑到它们在遇到与用于训练它们的数据非常不同的数据时的行为。我们将在本书中讨论这个问题。

2013 年,博士生 Matt Zeiler 和他的导师 Rob Fergus 发表了《可视化和理解卷积网络》,展示了如何可视化模型每一层学到的神经网络权重。他们仔细分析了赢得 2012 年 ImageNet 比赛的模型,并利用这一分析大大改进了模型,使他们能够赢得 2013 年的比赛!图 1-10 是他们发表的第一层权重的图片。

CNN 早期层的激活

图 1-10。CNN 第一层的激活(由 Matthew D. Zeiler 和 Rob Fergus 提供)

这张图片需要一些解释。对于每一层,具有浅灰色背景的图像部分显示了重建的权重,底部较大的部分显示了与每组权重最匹配的训练图像部分。对于第一层,我们可以看到模型发现了代表对角线、水平和垂直边缘以及各种梯度的权重。(请注意,对于每一层,只显示了部分特征;实际上,在所有层中有成千上万个特征。)

这些是模型为计算机视觉学习的基本构建块。它们已经被神经科学家和计算机视觉研究人员广泛分析,结果表明,这些学习的构建块与人眼的基本视觉机制以及在深度学习之前开发的手工计算机视觉特征非常相似。下一层在图 1-11 中表示。

CNN 早期层的激活

图 1-11。CNN 第二层的激活(由 Matthew D. Zeiler 和 Rob Fergus 提供)

对于第 2 层,模型找到的每个特征都有九个权重重建示例。我们可以看到模型已经学会创建寻找角、重复线条、圆圈和其他简单模式的特征检测器。这些是从第一层中开发的基本构建块构建的。对于每个特征,图片右侧显示了与这些特征最匹配的实际图像的小块。例如,第 2 行第 1 列中的特定模式与日落相关的梯度和纹理相匹配。

图 1-12 显示了一篇论文中展示第 3 层特征重建结果的图片。

CNN 中间层的激活

图 1-12。CNN 第三层的激活(由 Matthew D. Zeiler 和 Rob Fergus 提供)

通过观察图片右侧,您可以看到特征现在能够识别和匹配更高级的语义组件,如汽车车轮、文字和花瓣。利用这些组件,第 4 层和第 5 层可以识别更高级的概念,如图 1-13 所示。

CNN 末端层的激活

图 1-13。CNN 的第四和第五层的激活(由 Matthew D. Zeiler 和 Rob Fergus 提供)

本文研究了一个名为AlexNet的旧模型,该模型只包含五层。自那时以来开发的网络可以有数百层 - 所以你可以想象这些模型开发的特征有多丰富!

当我们早期微调我们的预训练模型时,我们调整了最后几层关注的内容(花朵、人类、动物),以专注于猫与狗问题。更一般地,我们可以将这样的预训练模型专门用于许多不同的任务。让我们看一些例子。

图像识别器可以处理非图像任务

其中举了一些将非图像处理任务的对象转换成图像之后再处理的例子,例如声波转换为声谱。

术语回顾

我们刚刚涵盖了很多信息,让我们简要回顾一下。表 1-3 提供了一个方便的词汇表。

表 1-3. 深度学习词汇表

术语 意义
标签 我们试图预测的数据,比如“狗”或“猫”
架构 我们试图拟合的模型的 模板 ;即我们将输入数据和参数传递给的实际数学函数
模型 架构与特定一组参数的组合
参数 模型中改变任务的值,通过模型训练进行更新
拟合 更新模型的参数,使得使用输入数据的模型预测与目标标签匹配
训练 拟合 的同义词
预训练模型 已经训练过的模型,通常使用大型数据集,并将进行微调
微调 为不同任务更新预训练模型
纪元 一次完整通过输入数据
损失 衡量模型好坏的指标,选择以驱动通过 SGD 进行训练
指标 使用验证集衡量模型好坏的测量标准,选择供人类消费
验证集 从训练中保留的一组数据,仅用于衡量模型好坏
训练集 用于拟合模型的数据;不包括验证集中的任何数据
过拟合 以使模型 记住 输入数据的特定特征而不是很好地泛化到训练期间未见的数据的方式训练模型
CNN 卷积神经网络;一种特别适用于计算机视觉任务的神经网络

课后习题

  1. 你需要这些来进行深度学习吗?

    • 很多数学 F

    • 很多数据 T

    • 很多昂贵的电脑 F

    • 一个博士学位 F

  2. 列出深度学习现在是世界上最好的工具的五个领域。

    • 图像识别
    • 自然语言处理
    • 图像分割
    • 数据分析预测
    • 用户喜好推测
  3. 第一个基于人工神经元原理的设备的名称是什么?

    • Mark 1 感知机
  4. 根据同名书籍,分布式并行处理(PDP)的要求是什么?

    • 一组处理单元

    • 激活状态

    • 每个单元的输出函数

    • 单位之间的连接模式

    • 通过网络连接传播活动模式的传播规则

    • 将输入与单位的当前状态相结合以产生单位输出的激活规则

    • 通过经验修改连接模式的学习规则

    • 系统必须运行的环境

  1. 是什么两个理论误解阻碍了神经网络领域的发展?
    • 使用多层设备可以解决这些限制
    • 单层无法学习一些简单但关键的数学函数(如异或)。
  1. 打开一个笔记本并执行包含:1+1 的单元格。会发生什么?

    • 会output 2
  2. 跟随本章笔记本的精简版本中的每个单元格。在执行每个单元格之前,猜测会发生什么。

  3. 完成Jupyter Notebook 在线附录

  4. 为什么使用传统计算机程序来识别照片中的图像很困难?

    • 需要详细说明过程的每一个细微步骤。
  5. 塞缪尔所说的“权重分配”是什么意思?

    • 即通过某种方式(如随机初始化、梯度下降优化等)为权重这种参数赋予初始值,并在训练过程中调整它们,使模型能更准确地拟合数据。
  6. 在深度学习中,我们通常用什么术语来表示塞缪尔所说的“权重”?

    • 参数
  7. 画一幅总结塞缪尔对机器学习模型看法的图片。

  8. 为什么很难理解深度学习模型为什么会做出特定的预测?

    • 深度学习模型通过多层非线性变换提取抽象特征,导致决策过程高度复杂且缺乏直观解释性。
  9. 展示了一个定理的名称,该定理表明神经网络可以解决任何数学问题并达到任何精度水平是什么?

    • 度量标准
  10. 为了训练模型,您需要什么?

    • 大量数据 其中需要划分出训练集和数据集
  11. 反馈循环如何影响预测性警务模型的推出?

    • 基于过去的逮捕地点创建了一个预测性执法模型。实际上,这并不是在预测犯罪,而是在预测逮捕,因此部分地只是反映了现有执法过程中的偏见。然后执法人员可能会使用该模型来决定在哪里集中他们的执法活动,导致这些地区的逮捕增加。这些额外逮捕的数据将被反馈回去重新训练未来版本的模型。

    • 这是一个正反馈循环:模型被使用得越多,数据就变得越有偏见,使模型变得更加有偏见,依此类推。

    • 反馈循环也可能在商业环境中造成问题。例如,视频推荐系统可能会偏向于推荐由视频最大观看者消费的内容(例如,阴谋论者和极端分子倾向于观看比平均水平更多的在线视频内容),导致这些用户增加他们的视频消费量,进而导致更多这类视频被推荐。我们将在第三章中更详细地讨论这个话题。

  12. 我们在猫识别模型中总是需要使用 224×224 像素的图像吗?

    • 不需要这是旧版本的遗留问题,在现在像素越高效果就会越好但所需要的时间就会更多
  13. 分类和回归之间有什么区别?
    • 分类预测离散类别(如是否垃圾邮件),回归预测连续数值(如房价)
  14. 什么是验证集?什么是测试集?为什么我们需要它们?
    • 简单来说就是一组模型从未见过的数据用来验证模型的训练效果,防止少量数据过度训练后导致过拟合,使得模型仅仅只是记住训练集而已。
    • 验证集用于调参和模型选择,测试集用于最终评估模型泛化性能;两者分离以避免过拟合和确保评估客观性。
  15. 如果不提供验证集,fastai 会怎么做?

    • 在valid_pct`默认设置比率fastai会将一定比率的训练集用作测试
  16. 我们总是可以使用随机样本作为验证集吗?为什么或为什么不?

    • 不可以 一个随机子集是一个糟糕的选择(填补缺失太容易,且不代表你在生产中所需的)对于时间序列,选择数据的随机子集既太容易(你可以查看你试图预测的日期之前和之后的数据),又不代表大多数业务用例(在这些用例中,你使用历史数据构建模型以供将来使用)。
  17. 什么是过拟合?举个例子。

    • 即使您的模型尚未完全记住所有数据,在训练的早期阶段可能已经记住了其中的某些部分。因此,您训练的时间越长,您在训练集上的准确性就会越好;验证集的准确性也会在一段时间内提高,但最终会开始变差,因为模型开始记住训练集而不是在数据中找到可泛化的潜在模式。
  18. 什么是度量?它与损失有什么不同?
    • 度量标准是一个函数,使用验证集来衡量模型预测的质量,并将在每个时期结束时打印出来。在这种情况下,我们使用error_rate,这是 fastai 提供的一个函数,它正是它所说的:告诉您验证集中有多少百分比的图像被错误分类。分类的另一个常见度量标准是accuracy(即1.0 - error_rate)。fastai 提供了许多其他度量标准,这将在本书中讨论。
    • 性能的度量被称为损失
  19. 预训练模型如何帮助?
    • 预训练模型通过迁移学习大幅减少新任务所需数据和训练时间,同时提升模型性能。
  20. 模型的“头”是什么?
    • 使用预训练模型时,cnn_learner将移除最后一层,因为该层始终是针对原始训练任务(即 ImageNet 数据集分类)专门定制的,并将其替换为一个或多个具有随机权重的新层,适合您正在处理的数据集的大小。模型的最后部分被称为头。
  21. CNN 的早期层找到了什么样的特征?后期层呢?

    • CNN 早期层的激活
    图 1-10。CNN 第一层的激活(由 Matthew D. Zeiler 和 Rob Fergus 提供)

    这张图片需要一些解释。对于每一层,具有浅灰色背景的图像部分显示了重建的权重,底部较大的部分显示了与每组权重最匹配的训练图像部分。对于第一层,我们可以看到模型发现了代表对角线、水平和垂直边缘以及各种梯度的权重。(请注意,对于每一层,只显示了部分特征;实际上,在所有层中有成千上万个特征。)
    通过观察图片右侧,您可以看到特征现在能够识别和匹配更高级的语义组件,如汽车车轮、文字和花瓣。利用这些组件,第 4 层和第 5 层可以识别更高级的概念,如图 1-13 所示。

    CNN 末端层的激活

    图 1-13。CNN 的第四和第五层的激活(由 Matthew D. Zeiler 和 Rob Fergus 提供)
  22. 图像模型仅对照片有用吗?

    • 否 你可以将相关的目标转换为图像后进行分析
  23. 什么是架构?

    • 我们试图拟合的模型的 模板 ;即我们将输入数据和参数传递给的实际数学函数
  24. 什么是分割?

    • 区分图片中的不同物体
  25. y_range 用于什么?什么时候需要它?
    • 预测的是一个连续数值,而不是一个类别时。告知我们的目标范围。
  26. 什么是超参数?
    • 它们是关于参数的参数,因为它们是控制权重参数含义的高级选择。
  27. 在组织中使用 AI 时避免失败的最佳方法是什么?
    • 真正理解测试和验证集以及它们的重要性

      进一步研究

每章还有一个“进一步研究”部分,提出了一些在文本中没有完全回答的问题,或者给出了更高级的任务。这些问题的答案不在书的网站上;您需要自己进行研究!

  1. 为什么 GPU 对深度学习有用?CPU 有什么不同之处,为什么对深度学习效果不佳?

  2. 试着想出三个反馈循环可能影响机器学习使用的领域。看看是否能找到实践中发生这种情况的文档示例。