R 深度学习第二版(三)
在之前的章节中,我们完全从头编写了自己的训练循环。这样做为您提供了最大的灵活性,但同时您需要编写大量的代码,同时错过了许多方便的 fit()功能,例如回调或分布式训练的内置支持。如果你需要自定义训练算法,但仍想利用内置的 Keras 训练逻辑的优势,那么在 fit()和从头编写训练循环之间实际上有一种中间状态:你可以提供自定义训练步骤功能,让框架完成其他工作。您可以通过覆盖 Model 类的 tr
原文:
zh.annas-archive.org/md5/e4fa128a428bebc7c809e7bf24e4df20译者:飞龙
第七章:使用 Keras:深入研究
本章内容
-
使用 keras_model_sequential()、功能 API 和模型子类化创建 Keras 模型
-
使用内置的 Keras 训练和评估循环
-
使用 Keras 回调自定义培训
-
使用 TensorBoard 监控培训和评估指标
-
从头开始编写训练和评估循环
现在您已经具备了一些使用 Keras 的经验——您熟悉序列模型、密集层和用于训练、评估和推理的内置 API——compile()、fit()、evaluate()和 predict()。您甚至在第三章中学习了如何使用 new_layer_class()创建自定义层,以及如何使用 TensorFlow GradientTape() 实现逐步训练循环。
在接下来的章节中,我们将深入研究计算机视觉、时间序列预测、自然语言处理和生成深度学习等复杂应用。这些复杂的应用需要多于一个 keras_model_sequential()架构和默认的 fit()循环。因此,让我们先将你变成一个 Keras 专家!在本章中,您将获得有关使用 Keras API 的关键方法的完整概述:这是您需要处理接下来会遇到的高级深度学习用例的所有内容。
7.1 工作流的多样性
Keras API 的设计以逐渐揭示复杂性的原则为指导:让开始变得容易,但让处理高复杂度的用例成为可能,每个步骤只需要进行渐进式的学习。简单的用例应该易于接近,并且任意高级工作流程都应该是可能的:无论您想要做什么多么具有特色和复杂,都应该有一条明确的路径,它建立在您从较简单的工作流程中学到的各种事情之上。这意味着您可以从初学者成长为专家,仍然使用相同的工具,只是使用不同的方式。
因此,Keras 没有单一的“真正”使用方式。相反,Keras 提供了一系列工作流,从非常简单到非常灵活。有不同的方法来构建 Keras 模型,以及不同的训练方法,以满足不同的需求。因为所有这些工作流都基于共享的 API,例如层和模型,所以任何工作流的组件都可以在任何其他工作流中使用,它们都可以相互通信。
7.2 构建 Keras 模型的不同方式
在 Keras 中,有三个 API 可以用于构建模型(见图 7.1):
-
Sequential 模型是最易接近的 API,它基本上是一个列表。因此,它仅限于简单的层堆栈。
-
功能 API注重于类似于图形的模型架构。它代表了易用性和灵活性之间的不错平衡,因此是最常用的模型构建 API。
-
模型子类化 是一个低级选项,你需要从头开始自己编写一切。这是理想的选择,如果你想要对每一件事情都有完全的控制。然而,你将无法访问许多内置的 Keras 功能,并且更容易犯错误。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0186-01.jpg
图 7.1 逐渐展示模型构建的复杂性
7.2.1 Sequential 模型
构建 Keras 模型的最简单方式是使用 keras_model_sequential(),这是你已经了解的。
清单 7.1 keras_model_sequential()
library(keras)
model <- keras_model_sequential() %>%
layer_dense(64, activation = “relu”) %>%
layer_dense(10, activation = “softmax”)
注意,可以使用 %>% 逐步构建相同的模型。
清单 7.2 逐步构建 Sequential 模型
model <- keras_model_sequential()
model %>% layer_dense(64, activation = “relu”)
model %>% layer_dense(10, activation = “softmax”)
在第四章中,你看到层是在第一次调用时构建的(也就是说,创建它们的权重)。这是因为层的权重形状取决于它们的输入形状:直到输入形状被知道,它们才能被创建。
因此,上述的 Sequential 模型在你实际上对其使用一些数据,或者使用其 build() 方法并指定输入形状之前,是没有任何权重的(清单 7.3)。
清单 7.3 尚未构建的模型没有权重
model$weights➊
Error in py_get_attr_impl(x, name, silent):
ValueError: 模型 sequential_1 的权重尚未创建。当模型首次根据输入进行调用或调用 build() 并指定 input_shape 时,才会创建权重。
➊ 此时,模型尚未构建。
清单 7.4 第一次调用模型以构建它
model$build(input_shape = shape(NA, 3))➊
str(model$weights)➋
长度为 4 的列表
$ :<tf.Variable ‘dense_2/kernel:0’ shape=(3, 64) dtype=float32, numpy=…>
$ :<tf.Variable ‘dense_2/bias:0’ shape=(64) dtype=float32, numpy=…>
$ :<tf.Variable ‘dense_3/kernel:0’ shape=(64, 10) dtype=float32, numpy=…>
$ :<tf.Variable ‘dense_3/bias:0’ shape=(10) dtype=float32, numpy=…>
➊ 构建模型——现在模型将期望形状为 (3) 的样本。输入形状中的 NA 表示批量大小可以是任何值。
➋ 现在你可以检索模型的权重。
在模型构建完成后,你可以通过 print() 方法显示其内容,这对调试很有用。
清单 7.5 print() 方法
model
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0188-01.jpg
正如你所看到的,这个模型恰好被命名为“sequential_1”。你可以为 Keras 中的每一个东西都起名字——每一个模型,每一个层。
清单 7.6 使用 name 参数为模型和层命名
model <- keras_model_sequential(name = “my_example_model”)
model %>% layer_dense(64, activation = “relu”, name = “my_first_layer”)
model %>% layer_dense(10, activation = “softmax”, name = “my_last_layer”)
model$build(shape(NA, 3))
model
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0188-02.jpg
在逐步构建 Sequential 模型时,可以在每添加一层后打印当前模型的摘要非常有用。但是直到模型构建完成之前,您无法打印摘要!事实上,有一种方法可以动态构建您的 Sequential 模型:只需提前声明模型输入的形状即可。您可以通过将 input_shape 传递给 keras_model_sequential() 来实现此目的。
清单 7.7 预先指定您模型的输入形状
model <
使用 keras_model_sequential(input_shape = c(3)) %>%➋
layer_dense(64, activation = "relu")
➊ 提供 input_shape 以声明输入的形状。请注意,shape 参数必须是每个样本的形状,而不是一个批次的形状。
现在您可以使用 print() 跟踪随着您添加更多层,模型输出形状如何变化:
model
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0189-01.jpg
model %>% layer_dense(10, activation = "softmax")
model
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0189-02.jpg
这是在处理以复杂方式转换其输入的层时的一种非常常见的调试工作流程,例如您将在第八章中了解的卷积层。
7.2.2 Functional API
Sequential 模型易于使用,但其适用性极为有限:它只能表示具有单个输入和单个输出的模型,并以顺序方式一个接一个地应用层。在实践中,遇到具有多个输入(例如,图像及其元数据)、多个输出(关于数据您想要预测的不同事物)或非线性拓扑的模型是非常常见的。
在这种情况下,您将使用 Functional API 构建模型。这是您在野外遇到的大多数 Keras 模型使用的。这很有趣和强大——感觉就像玩乐高积木一样。
一个简单的例子
让我们从简单的东西开始:我们在上一节中使用的两层堆叠。其 Functional API 版本如下所示。
清单 7.8 具有两个 Dense 层的简单 Functional 模型
inputs <- layer_input(shape = c(3), name = "my_input")
features <- inputs %>% layer_dense(64, activation = "relu")
outputs <- features %>% layer_dense(10, activation = "softmax")
model <- keras_model(inputs = inputs, outputs = outputs)
让我们一步一步来。我们首先声明了一个 layer_input()(请注意,您也可以给这些输入对象命名,就像其他所有东西一样):
inputs <- layer_input(shape = c(3), name = "my_input")
此 inputs 对象保存有关模型将处理的数据的形状和 dtype 的信息:
inputs$shape➊
TensorShape([None, 3])
inputs$dtype➋
tf.float32
➊ 该模型将处理每个样本形状为 (3) 的批次。每个批次中的样本数量是可变的(由 None 批次大小指示)。
这些批次的 dtype 将为 float32。
我们称这样的对象为符号张量。它不包含任何实际数据,但它编码了模型在使用时将看到的实际数据张量的规格。它代表未来的数据张量。
接下来,我们创建一个层,并与输入组合:
features <- inputs %>% layer_dense(64, activation = “relu”)
在 Functional API 中,将符号张量传送到层构造函数会调用该层的 call() 方法。实质上,这就是发生的事情:
layer_instance <- layer_dense(units = 64, activation = “relu”) features <- layer_instance(inputs)
这与 Sequential API 不同,其中将层与模型组合(model %>% layer_dense())意味着这个:
layer_instance <- layer_dense(units = 64, activation = “relu”) model$add(layer_instance)
所有 Keras 层既可以在实际数据张量上调用,也可以在这些符号张量上调用。在后一种情况下,它们返回一个新的符号张量,具有更新的形状和 dtype 信息:
features$shape
TensorShape([None, 64])
注意,符号张量几乎可以与所有相同的 R 通用方法一起使用,如 eager 张量。这意味着你也可以像这样获取形状作为 R 整数向量:
dim(features)
[1] NA 64
在获得最终输出后,我们通过在 keras_model() 构造函数中指定其输入和输出来实例化模型:
outputs <- layer_dense(features, 10, activation = “softmax”)
model <- keras_model(inputs = inputs, outputs = outputs)
这是我们模型的概要:
model
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0191-01.jpg
多输入,多输出模型
与这个玩具模型不同,大多数深度学习模型看起来不像列表,而是像图。例如,它们可能具有多个输入或多个输出。正是对于这种模型,Functional API 才真正发挥作用。
假设你正在构建一个系统,根据优先级对客户支持票进行排名,并将其路由到适当的部门。你的模型有三个输入:
-
票的标题(文本输入)
-
票的文本正文(文本输入)
-
用户添加的任何标签(假定这里是 one-hot 编码的分类输入)
我们可以将文本输入编码为大小为 vocabulary_size 的 1 和 0 数组(有关文本编码技术的详细信息,请参见第十一章)。你的模型还有两个输出:
-
票的优先级分数,介于 0 和 1 之间的标量(sigmoid 输出)
-
应处理该票的部门(对部门集合进行 softmax)
你可以用几行代码使用 Functional API 构建此模型。
列表 7.9 多输入,多输出的 Functional 模型
vocabulary_size <- 10000
num_tags <- 100
num_departments <- 4
title <- layer_input(shape = c(vocabulary_size), name = “title”)➊
text_body <- layer_input(shape = c(vocabulary_size), name = “text_body”)➊
tags <- layer_input(shape = c(num_tags), name = “tags”)➊
features <-
layer_concatenate(list(title, text_body, tags)) %>%➋
layer_dense(64, activation = “relu”)➌
priority <- features %>%➍
layer_dense(1, activation = “sigmoid”, name = “priority”)
department <- features %>%➍
layer_dense(num_departments, activation = “softmax”, name = “department”)
model <- keras_model(➎
inputs = list(title, text_body, tags),
outputs = list(priority, department)
)
➊ 定义模型的输入。
➋ 将输入特征合并成单个张量,通过连接它们。
➌ 应用中间层将输入特征重新组合成更丰富的表示。
➍ 定义模型的输出。
➎ 通过指定其输入和输出来创建模型。
函数式 API 是一种简单、类似于 LEGO 但非常灵活的方式,可以定义这样的任意层级图。
训练多输入、多输出模型
您可以通过调用 fit()并传递输入和输出数据的列表来训练模型,这与训练 Sequential 模型的方式非常相似。这些数据列表应与您传递给 keras_model()构造函数的输入的顺序相同。
示例 7.10 通过提供输入和目标数组列表来训练模型
num_samples <- 1280
random_uniform_array <- function(dim)
array(runif(prod(dim)), dim)
random_vectorized_array <- function(dim)
array(sample(0:1, prod(dim), replace = TRUE), dim)
title_data <- random_vectorized_array(c(num_samples, vocabulary_size))➊
text_body_data <- random_vectorized_array(c(num_samples, vocabulary_size))➊
tags_data <- random_vectorized_array(c(num_samples, num_tags))➊
priority_data <- random_vectorized_array(c(num_samples, 1))➋
department_data <- random_vectorized_array(c(num_samples, num_departments))➋
model %>% compile(
optimizer = “rmsprop”,
loss = c(“mean_squared_error”, “categorical_crossentropy”),
metrics = c(“mean_absolute_error”, “accuracy”)
)
model %>% fit(
x = list(title_data, text_body_data, tags_data),
y = list(priority_data, department_data),
epochs = 1
)
model %>% evaluate(x = list(title_data, text_body_data, tags_data),
y = list(priority_data, department_data))
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0193-01.jpg
c(priority_preds, department_preds) %<-% {➌
model %>% predict(list(title_data, text_body_data, tags_data))
}
➊ 虚拟的输入数据
➋ 虚拟的目标数据
➌ 要在同一表达式中使用%<-%和%>%,您需要用{}或()将管道序列包装起来,以覆盖默认的运算符优先级。
如果您不想依赖输入顺序(例如,因为有多个输入或输出),您也可以利用您给输入形状和输出层指定的名称,并通过命名列表传递数据。
重要提示:使用命名列表时,列表的顺序不能保证保留下来。请务必通过位置或名称跟踪项目,但不能混合使用两者。
示例 7.11 通过提供命名输入和目标数组列表来训练模型
model %>%
compile(optimizer = “rmsprop”,
loss = c(priority = “mean_squared_error”,
department = “categorical_crossentropy”),
metrics = c(priority = “mean_absolute_error”,
department = “accuracy”))
model %>%
fit(list(title = title_data,
text_body = text_body_data,
标签 = tags_data),
list(priority = priority_data,
department = department_data), epochs = 1)
model %>%
评估(list(title = title_data,
text_body = text_body_data,
标签 = tags_data),
list(priority = priority_data,
department = department_data))
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0194-02.jpg
c(priority_preds, department_preds) %<-%
预测(model, list(title = title_data,
text_body = text_body_data,
标签 = tags_data))
函数 API 的威力:访问层连接性
功能模型是一个明确的图形数据结构。这使得可以检查层如何连接,并重复使用以前的图形节点(它们是层输出)作为新模型的一部分。它还很好地适应了大多数研究人员在思考深度神经网络时使用的“心理模型”:层的图形。这使两个重要用例成为可能:模型可视化和特征提取。
让我们可视化刚刚定义的模型的连接性(模型的 拓扑)。您可以使用 plot() 方法将功能模型绘制为图形(参见图 7.2):
plot(model)
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0194-01.jpg
图 7.2 由我们的票务分类器模型 plot(model) 生成的图表
您可以在模型的每一层中添加此图表的输入和输出形状,这在调试期间可能会有所帮助(参见图 7.3):
plot(model, show_shapes = TRUE)
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0195-01.jpg
图 7.3 添加形状信息的模型图
张量形状中的“None”表示批处理大小:该模型允许任意大小的批处理。
访问层连接性也意味着您可以检查和重复使用图中的单个节点(层调用)。模型 l a y e r s 模型属性提供了组成模型的层的列表,对于每个层,您可以查询 l a y e r layers 模型属性提供了组成模型的层的列表,对于每个层,您可以查询 layer layers模型属性提供了组成模型的层的列表,对于每个层,您可以查询layerinput 和 layer$output。
列表 7.12 在功能模型中检索层的输入或输出
str(model$layers)
7 的列表
$ :<keras.engine.input_layer.InputLayer object at 0x7fc962da63a0>
$ :<keras.engine.input_layer.InputLayer object at 0x7fc962da6430>
$ :<keras.engine.input_layer.InputLayer object at 0x7fc962da68e0>
$ :<keras.layers.merge.Concatenate object at 0x7fc962d2e130>
$ :<keras.layers.core.dense.Dense object at 0x7fc962da6c40>
$ :<keras.layers.core.dense.Dense object at 0x7fc962da6340>
$ :<keras.layers.core.dense.Dense object at 0x7fc962d331f0>
str(model l a y e r s [ [ 4 ] ] layers[[4]] layers[[4]]input)
3 的列表
$ :<KerasTensor: shape=(None, 10000) dtype=float32 (created by layer
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/common01.jpg ‘title’)>
$ :<KerasTensor: shape=(None, 10000) dtype=float32 (created by layer
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/common01.jpg ‘text_body’)>
$ :<KerasTensor: shape=(None, 100) dtype=float32 (created by layer ‘tags’)>
str(model l a y e r s [ [ 4 ] ] layers[[4]] layers[[4]]output)
<KerasTensor: shape=(None, 20100) dtype=float32 (created by layer
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/common01.jpg ‘concatenate’)>
这使您能够进行 特征提取,创建重用另一个模型的中间特征的模型。
假设你想要在前一个模型中添加另一个输出——你想要估计给定问题票的解决时间,一种困难评级。你可以通过三个类别的分类层来实现这一点:“快速”,“中等”和“困难”。你不需要从头开始重新创建和重新训练一个模型。你可以从你以前模型的中间特征开始,因为你可以像这样访问它们。
Listing 7.13 通过重用中间层输出创建一个新模型
features <- model l a y e r s [ [ 5 ] ] layers[[5]] layers[[5]]output➊
difficulty <- features %>%
layer_dense(3, activation = “softmax”, name = “difficulty”)
new_model <- keras_model(
inputs = list(title, text_body, tags),
outputs = list(priority, department, difficulty)
)
➊ layer[[5]] 是我们的中间密集层。您也可以使用 get_layer() 按名称检索层。
让我们绘制我们的新模型(见图 7.4):
plot(new_model, show_shapes = TRUE)
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0196-01.jpg
图 7.4 我们的新模型绘图:更新的工单分类器
7.2.3 子类化 Model 类
你应该知道的最后一个模型构建模式是最高级的模型子类化。你在第三章学习过如何使用 new_layer_class() 子类化 Layer 类并创建自定义层。使用 new_model_class() 子类化 Model 类非常相似:
-
在 initialize() 方法中,定义模型将使用的层。
-
在 call() 方法中,定义模型的前向传播,重用先前创建的层。
-
实例化你的子类,并在数据上调用它以创建它的权重
重写我们之前的示例作为子类模型
让我们看一个简单的例子:我们将使用 new_model_class() 重新实现客户支持票务管理模型来定义一个 Model 子类。
Listing 7.14 一个简单的子类模型
CustomerTicketModel <- new_model_class(
classname = “CustomerTicketModel”,
initialize = function(num_departments) {
super$initialize()➊
self$concat_layer <- layer_concatenate()
self$mixing_layer <-➋
layer_dense(units = 64, activation = “relu”)
self$priority_scorer <-
layer_dense(units = 1, activation = “sigmoid”)
self$department_classifier <-
layer_dense(units = num_departments, activation = “softmax”)
},
call = function(inputs) {➌
title <- inputs$title➍
text_body <- inputs$text_body
tags <- inputs$tags
features <- list(title, text_body, tags) %>%
self$concat_layer() %>%
self$mixing_layer()
priority <- self$priority_scorer(features)
department <- self$department_classifier(features)
list(priority, department)
}
)
➊ 别忘了调用 super$initialize()!
➋ 在构造函数中定义子层。请注意,我们在这里指定了 units 参数名称,以便我们得到一个层实例。
➌ 在 call() 方法中定义前向传播。
➍ 对于输入,我们将提供一个带有名称的列表给模型。
我们在第三章中实现了 Model 的最简版本。要注意的主要事项是,我们正在定义一个自定义类,即我们的模型,它是 Model 子类。正如您将在接下来的章节中看到的那样,Model 提供了许多方法和功能,您可以选择加入其中。
定义模型之后,您可以实例化它。请注意,它只会在首次在某些数据上调用它时创建其权重,就像层子类一样:
model <- CustomerTicketModel(num_departments = 4)
c(priority, department) %<-% model(list(title = title_data,
text_body = text_body_data,
tags = tags_data))
模型本质上是一种层(Layer)。这意味着,使用 create_layer_wrapper()就可以轻松地使模型具有与%>%很好地组合的能力,就像这样:
inputs <- list(title = title_data,
text_body = text_body_data,
tags = tags_data)
layer_customer_ticket_model <- create_layer_wrapper(CustomerTicketModel)
outputs <- inputs %>%
layer_customer_ticket_model(num_departments = 4)
c(priority, department) %<-% outputs
到目前为止,所有内容看起来与 Layer 子类化非常相似,这是您在第三章中遇到的工作流程。那么,“层”的子类和“模型”的子类之间有什么区别呢?答案很简单:一个“层”是用来创建模型的构建块,而一个“模型”是您将实际培训、导出推理等的最高级对象。换句话说,模型具有 fit()、evaluate()和 predict()方法,而层则没有。除此之外,这两个类几乎完全相同。(另一个区别是您可以将模型保存在磁盘上的文件中,我们将在几个章节中介绍这个功能。)您可以编译和训练 Model 子类,就像 Sequential 模型或 Functional 模型一样:
model %>%
compile(optimizer = “rmsprop”,
loss = c(“mean_squared_error”,
“categorical_crossentropy”),➊
metrics = c(“mean_absolute_error”, “accuracy”))
x <- list(title = title_data,➋
text_body = text_body_data,
tags = tags_data)
y <- list(priority_data, department_data)➌
model %>% fit(x, y, epochs = 1)
model %>% evaluate(x, y)
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0198-01.jpg
c(priority_preds, department_preds) %<-% {
model %>% predict(x)
}
➊ 您作为损失和指标参数传递的内容的结构必须严格匹配 call()方法返回的内容,即两个元素的列表。
➋ 输入数据的结构必须严格匹配 call()方法所期望的内容,即一个命名列表,其中包括标题(title)、正文(text_body)和标签(tags)。当按名称匹配时,列表顺序将被忽略!
➌ 目标数据的结构必须严格匹配 call()方法返回的内容,即两个元素的列表。
Model 子类化流程是构建模型最灵活的方法。它使您能够构建无法表示为层的有向无环图的模型,例如,模型的 call()方法在 for 循环内使用层,甚至会递归调用它们。任何事情都可能发生——您有权决定。
警告:子类化模型不支持的内容
这种自由是有代价的:通过子类模型,您需要负责更多的模型逻辑,这意味着您的潜在错误范围要大得多。因此,您将需要更多的调试工作。您正在开发一个新的类对象,而不仅仅是将乐高积木拼凑在一起。
Functional 和子类模型在性质上也有很大不同。Functional 模型是一个显式数据结构——层的图形,您可以查看、检查和修改。而子类模型是一组 R 代码——一个具有 call() 方法的类,该方法是一个 R 函数。这就是子类化工作流程的灵活性来源——您可以编写任何功能性代码——但它也引入了新的限制。
例如,因为层之间连接的方式被隐藏在 call() 方法的主体内部,您无法访问该信息。调用 summary() 将不显示层连接性,并且您无法通过 plot() 绘制模型拓扑。同样,如果您有一个子类模型,您无法访问层图的节点来进行特征提取,因为简单地没有图。一旦模型被实例化,其正向传递就变成了一个完全的黑盒。
7.2.4 混合和匹配不同的组件
关键是,选择这些模式之一——Sequential 模型、Functional API 或 Model 子类化——不会将您排除在其他模式之外。Keras API 中的所有模型都可以与彼此平滑地互操作,无论它们是 Sequential 模型、Functional 模型还是从头编写的子类模型。它们都是同一工作流谱系的一部分。例如,您可以在 Functional 模型中使用一个子类化的层或模型。
列表 7.15 创建一个包含子类化模型的 Functional 模型
ClassifierModel <- new_model_class(
classname = “Classifier”,
initialize = function(num_classes = 2) {
super$initialize()
if (num_classes == 2) {
num_units <- 1
activation <- “sigmoid”
} else {
num_units <- num_classes
activation <- “softmax”
}
self$dense <- layer_dense(units = num_units, activation = activation)
},
call = function(inputs)
self$dense(inputs)
)
inputs <- layer_input(shape = c(3))
classifier <- ClassifierModel(num_classes = 10)
outputs <- inputs %>%
layer_dense(64, activation = “relu”) %>%
classifier()
model <- keras_model(inputs = inputs, outputs = outputs)
相反地,您可以在子类化层或模型中使用 Functional 模型的一部分。
列表 7.16 创建一个包含 Functional 模型的子类模型
inputs <- layer_input(shape = c(64))
outputs <- inputs %>% layer_dense(1, activation = “sigmoid”)
binary_classifier <- keras_model(inputs = inputs, outputs = outputs)
MyModel <- new_model_class(
classname = “MyModel”,
initialize = function(num_classes = 2) {
super$initialize()
self$dense <- layer_dense(units = 64, activation = “relu”)
self$classifier <- binary_classifier
},
call = function(inputs) {
inputs %>%
self$dense() %>%
self$classifier()
}
)
model <- MyModel()
7.2.5 记住:用对的工具做对的事情
你已经学习了一系列构建 Keras 模型的工作流程,从最简单的序列模型到最高级的模型子类化。什么时候应该使用其中一种?每种都有其优缺点,选择最适合当前工作的一种。
总的来说,功能 API 为您提供了易于使用和灵活性的很好的平衡。它还直接给您访问层连接性的能力,对于模型绘制或特征提取等用例非常有用。如果您可以使用功能 API,即,如果您的模型可以表示为层的有向无环图,那么我建议您使用功能 API 而不是模型子类化。
接下来,本书中的所有示例都将使用函数式 API,仅因为我们将使用的所有模型都可以表示为层图。我们还将经常使用子类化层(使用 new_layer_class())。总的来说,在包括子类化层的功能模型中使用,既具有高度的开发灵活性,同时又保留了功能 API 的优势,是最佳选择。
7.3 使用内置的训练和评估循环
逐步透明化复杂性的原则——从极简单到任意灵活的不同工作流的访问,一步一步——也适用于模型训练。Keras 为您提供了不同的模型训练工作流。它们可以简单到在数据上调用 fit(),也可以高级到从头编写新的训练算法。
你已经熟悉了 compile()、fit()、evaluate()、predict() 的工作流程。作为提醒,请看下面的清单。
清单 7.17 标准工作流程:compile()、fit()、evaluate()、predict()
get_mnist_model <- function() {➊
inputs <- layer_input(shape = c(28 * 28))
outputs <- inputs %>%
layer_dense(512, activation = “relu”) %>%
layer_dropout(0.5) %>%
layer_dense(10, activation = “softmax”)
keras_model(inputs, outputs)
}
c(c(images, labels), c(test_images, test_labels)) %<-%➋
dataset_mnist()
images <- array_reshape(images, c(-1, 28 * 28)) / 255
test_images <- array_reshape(test_images, c(-1, 28 * 28)) / 255
val_idx <- seq(10000)
val_images <- images[val_idx, ]
val_labels <- labels[val_idx]
train_images <- images[-val_idx, ]
train_labels <- labels[-val_idx]
model <- get_mnist_model()
model %>% compile(optimizer = “rmsprop”,➌
loss = “sparse_categorical_crossentropy”,
metrics = “accuracy”)
model %>% fit(train_images, train_labels,➍
epochs = 3,
validation_data = list(val_images, val_labels))
test_metrics <- model %>% evaluate(test_images, test_labels)➎
predictions <- model %>% predict(test_images)➏
➊ 创建模型(我们将其分解成一个单独的函数,以便以后重用)。
➋ 预留一部分数据作为验证,加载你的数据。
➌ 通过指定优化器、最小化的损失函数和要监控的指标来编译模型。
➍ 使用 fit()训练模型,可以选择提供验证数据以监控在看不见的数据上的性能。
➎ 使用 evaluate()计算新数据的损失和指标。
➏ 使用 predict()计算新数据的分类概率。
有几种方法可以自定义这个简单的工作流程:
-
提供你自己的自定义指标。
-
在 fit()方法中传递callbacks以安排在训练过程中特定时间点执行的操作。
让我们来看一下这些。
7.3.1 编写自己的指标
指标是衡量模型性能的关键,尤其是衡量其在训练数据和测试数据上性能差异的指标。分类和回归常用的指标已经是 keras 包的一部分,都以 metric_ 前缀开头,大多数情况下你会使用它们。但是,如果你要做一些超出寻常的事情,就需要编写自己的指标。这很简单!
一个 Keras 指标是 Keras Metric 类的子类。和层一样,指标有一个存储在 TensorFlow 变量中的内部状态。与层不同的是,这些变量不是通过反向传播更新的,所以必须自己编写状态更新逻辑,这发生在 update_state()方法中。例如,这是一个简单的自定义指标,用于测量均方根误差(RMSE)。
图 7.18 使用 Metric 类的子类实现自定义指标
library(tensorflow)➊
metric_root_mean_squared_error <- new_metric_class( classname➋
= “RootMeanSquaredError”,
initialize = function(name = “rmse”, …) {➌
super$initialize(name = name, …)
self m s e s u m < − s e l f mse_sum <- self msesum<−selfadd_weight(name = “mse_sum”,
initializer = “zeros”,
dtype = “float32”)
self t o t a l s a m p l e s < − s e l f total_samples <- self totalsamples<−selfadd_weight(name = “total_samples”,
initializer = “zeros”,
dtype = “int32”)
},
update_state = function(y_true, y_pred, sample_weight = NULL) {➍
num_samples <- tf$shape(y_pred)[1]
num_features <- tf$shape(y_pred)[2]
y_true <- tf$one_hot➎(y_true, depth = num_features)➏
mse <- sum((y_true - y_pred) ^ 2)➐
self m s e s u m mse_sum msesumassign_add(mse)
self t o t a l s a m p l e s total_samples totalsamplesassign_add(num_samples)
},
result = function() {
sqrt(self$mse_sum /
tf c a s t ( s e l f cast(self cast(selftotal_samples, “float32”))➑
},
reset_state = function() {
self m s e s u m mse_sum msesumassign(0)
self t o t a l s a m p l e s total_samples totalsamplesassign(0L)
}
)
➊ 我们将使用 tf 模块函数。
➋ 定义一个新的类,它是 Metric 基类的子类。
➌ 在构造函数中定义状态变量。像层一样,你可以访问 add_weight()方法。
➍ 在 update_state()中实现状态更新逻辑。y_true 参数是一个 batch 的目标(或标签),而 y_pred 表示模型的相应预测。你可以忽略 sample_weight 参数——我们这里不会使用它。
➎ 记住,tf 模块函数使用基于 0 的计数惯例。y_true 中的值为 0 在 one-hot 向量的第一个位置上放置 1。
➏ 为了匹配我们的 MNIST 模型,我们期望分类预测和整数标签。
➐ 我们也可以将此写为 tf r e d u c e s u m ( t f reduce_sum (tf reducesum(tfsquare(tf$subtract(y_true, y_pred)))。
➑ 将 total_samples 强制转换为与 mse_sum 相匹配的 dtype。
注意,在 update_state() 中我们使用 tf s h a p e ( y p r e d ) 而不是 y p r e d shape(y_pred) 而不是 y_pred shape(ypred)而不是ypredshape。tf s h a p e ( ) 返回一个 t f . T e n s o r 形式的形状,而不是像 y p r e d shape() 返回一个 tf.Tensor 形式的形状,而不是像 y_pred shape()返回一个tf.Tensor形式的形状,而不是像ypredshape 那样返回 tf.TensorShape。tf$shape() 允许 tf_function() 编译一个可以操作具有未定义形状的张量的函数,例如我们这里的输入具有未定义的批量维度。我们很快就会了解更多关于 tf_function() 的知识。
您可以使用 result() 方法返回指标的当前值:
result = function()
sqrt(self$mse_sum /
tf c a s t ( s e l f cast(self cast(selftotal_samples, “float32”))
同时,您还需要暴露一种方法来重置指标状态,而无需重新实例化它——这使得可以在不同的训练周期或在训练和评估期间使用相同的指标对象。您可以使用 reset_state() 方法来实现这一点:
reset_state = function() {
self m s e s u m mse_sum msesumassign(0)
self t o t a l s a m p l e s total_samples totalsamplesassign(0L)➊
}
➊ 注意我们传递的是一个整数,因为 total_samples 具有整数类型。
自定义指标可以像内置指标一样使用。让我们试驾我们自己的指标:
model <- get_mnist_model()
model %>%
compile(optimizer = “rmsprop”,
loss = “sparse_categorical_crossentropy”,
metrics = list(“accuracy”, metric_root_mean_squared_error()))
model %>%
fit(train_images, train_labels,
epochs = 3,
validation_data = list(val_images, val_labels))
test_metrics <- model %>% evaluate(test_images, test_labels)
现在,您可以看到 fit() 进度条显示您模型的 RMSE。
使用回调函数
使用 fit() 方法对大型数据集进行数十个时期的训练运行有点像发射一架纸飞机:过了最初的冲动,您就无法控制其轨迹或着陆点。如果您想避免不良结果(从而浪费纸飞机),最明智的做法不是使用纸飞机,而是使用一个可以感知环境、将数据发送回其操作者并根据当前状态自动做出转向决策的无人机。Keras 的 回调 API 将帮助您将对 fit(model) 的调用从纸飞机变成一个聪明的、自主的无人机,它可以自我检查并根据当前状态动态采取行动。
回调是一个对象(实现特定方法的类实例),它在调用 fit() 方法时被传递给模型,并在训练过程中的各个时刻被模型调用。它可以访问模型及其性能的所有可用数据,并且可以采取行动:中断训练,保存模型,加载不同的权重集,或者以其他方式更改模型的状态。以下是您可以使用回调的一些示例:
-
模型检查点—在训练过程中的不同时间点保存模型的当前状态。
-
提前停止训练——当验证损失不再改善时中断训练(当然,同时保存训练过程中表现最佳的模型)。
-
动态调整训练过程中特定参数的值——比如优化器的学习率。
-
记录训练和验证指标,或者可视化模型学习到的表示的更新过程——fit() 的进度条实际上就是一个回调!
keras 包中包含了许多内置回调(这不是一个详尽列表):
callback_model_checkpoint()
callback_early_stopping()
callback_learning_rate_scheduler()
callback_reduce_lr_on_plateau()
callback_csv_logger()
…
让我们回顾一下其中的两个示例——callback_early_stopping() 和 callback_model_checkpoint(),以便了解它们的使用方法。
提前停止和模型检查点回调
在训练模型时,有很多事情一开始无法预测。特别是,你无法确定要达到最佳验证损失需要多少个 epoch。我们迄今为止的示例采用的策略是训练足够多的 epoch,直到开始出现过拟合,利用第一次运行来确定需要训练的适当 epoch 数,然后最终从头开始启动新的训练。当然,这种方法是浪费的。更好的处理方法是在测量到验证损失不再改善时停止训练。这可以使用 callback_early_stopping() 实现。
提前停止回调一旦自定监控的指标连续固定轮不再改善,就中断训练。例如,该回调可让你一旦开始出现过拟合,就中断训练,从而避免必须减少训练轮数重新训练模型。此回调通常与 callback_model_checkpoint() 结合使用,后者让你在训练期间不断保存模型(可选地,仅保存到目前为止表现最佳的模型,即在一个 epoch 结束时获得最佳性能的模型版本)。
示例 7.19 在 fit() 方法中使用 callbacks 参数
callbacks_list <- list(
callback_early_stopping(
monitor = “val_accuracy”, patience = 2),➊
callback_model_checkpoint(➋
filepath = “checkpoint_path.keras”,➌
monitor = “val_loss”, save_best_only = TRUE)➍
)
model <- get_mnist_model()
model %>% compile(
optimizer = “rmsprop”,
loss = “sparse_categorical_crossentropy”,
metrics = “accuracy”)➎
model %>% fit(
train_images, train_labels,
epochs = 10,
callbacks = callbacks_list,➏
validation_data = list(val_images, val_labels))➐
➊ 当验证准确率连续两轮停止改善时中断训练。
➋ 在每个 epoch 后保存当前的权重。
➌ 目标模型文件的保存路径
➍ 这两个参数意味着您只有在 val_loss 有所改善时才会重写模型文件,这样您就可以保留训练过程中见过的最佳模型。
➎ 您监控准确率,所以它应该是模型的度量值之一。
➏ 回调函数通过fit()中的 callbacks 参数传递给模型,该参数接受一个回调函数列表。您可以传递任意数量的回调函数。
➐ 请注意,由于回调函数会监控 val_loss 和 val_accuracy,您需要将 validation_data 传递给 fit()函数的调用。
注意,您也可以在训练后手动保存模型,只需调用 save_model_tf(model,‘my_checkpoint_path’)。要重新加载保存的模型,只需使用以下命令:
model <- load_model_tf(“checkpoint_path.keras”)
7.3.3 编写自己的回调函数
如果您需要在训练过程中执行某个特定的操作,而这个操作不包含在内置的回调函数中,您可以编写自己的回调函数。回调函数通过子类化 Keras Callback 类并使用 new_callback_class()来实现。然后,您可以实现以下任意数量的透明命名方法,在训练的不同阶段调用:
on_epoch_begin(epoch, logs)➊
on_epoch_end(epoch, logs)➋
on_batch_begin(batch, logs)➌
在每个 batch 结束时调用➍
on_train_begin(logs)➎
on_train_end(logs)➏
➊ 在每个 epoch 开始时调用
➋ 在每个 epoch 结束时调用
➌ 在处理每个 batch 之前调用
➍ 在处理每个 batch 之后调用
➎ 在训练开始时调用
➏ 在训练结束时调用
这些方法都带有 logs 参数,它是一个带有关于上一批次、批次或训练运行的信息的有命名的列表,包括训练和验证的指标等。on_epoch_*和 on_batch_*方法还将 epoch 或 batch 索引作为它们的第一个参数(一个整数)。
这是一个简单的示例,它在训练期间保存了每个 batch 的损失值列表,并在每个 epoch 结束时保存了这些值的图形。
清单 7.20:通过子类化 Callback 类创建自定义回调函数
callback_plot_per_batch_loss_history <- new_callback_class(
classname = “PlotPerBatchLossHistory”,
initialize = function(file = “training_loss.pdf”) {
private$outfile <- file
},
on_train_begin = function(logs = NULL) {
private$plots_dir <- tempfile()
dir.create(private$plots_dir)
private$per_batch_losses <-
fastmap::faststack(init = self p a r a m s params paramssteps)
},
on_epoch_begin = function(epoch, logs = NULL) {
private p e r b a t c h l o s s e s per_batch_losses perbatchlossesreset()
},
on_batch_end = function(batch, logs = NULL) {
private p e r b a t c h l o s s e s per_batch_losses perbatchlossespush(logs$loss)
},
on_epoch_end = function(epoch, logs = NULL) {
losses <- as.numeric(private p e r b a t c h l o s s e s per_batch_losses perbatchlossesas_list())
filename <- sprintf(“epoch_%04i.pdf”, epoch)
filepath <- file.path(private$plots_dir, filename)
pdf(filepath, width = 7, height = 5)
on.exit(dev.off())
plot(losses, type = “o”,
ylim = c(0, max(losses)),
panel.first = grid(),
main = sprintf("每批次的训练损失\n(第 %i 个周期)", epoch),
xlab = "Batch", ylab = "Loss")
},
on_train_end = function(logs) {
private$per_batch_losses <- NULL
plots <- sort(list.files(private$plots_dir, full.names = TRUE))
qpdf::pdf_combine(plots, private$outfile)
unlink(private$plots_dir, recursive = TRUE)
}
)
使用 fastmap::faststack() 增长 R 对象
使用 c() 或 <- 来增长 R 向量通常很慢,最好避免。在这个示例中,我们使用 fastmap::faststack() 来更有效地收集每个批次的损失。
在自定义类方法中的 private 和 self
在所有之前的例子中,我们使用 self 来跟踪实例属性,但在这个回调示例中,我们使用了 private。有什么区别呢?像 self$foo 这样的属性也可以直接从类实例 instance$foo 访问。然而,private 的属性只能从类方法内部访问。
另一个重要的区别是,Keras 自动将分配给 self 的所有内容转换为 Keras 本地格式。这有助于 Keras 自动查找例如与自定义层关联的所有 tf.Variables。然而,这种自动转换有时会对性能产生影响,甚至对某些类型的 R 对象(如 faststack())失败。另一方面,private 是一个纯 R 环境,Keras 不会对其进行任何更改。只有您编写的类方法才会直接与私有属性交互。
model <- get_mnist_model()
model %>% compile(optimizer = "rmsprop",
loss = "sparse_categorical_crossentropy",
metrics = "accuracy")
model %>% fit(train_images, train_labels,
epochs = 10,
callbacks = list(callback_plot_per_batch_loss_history()),
validation_data = list(val_images, val_labels))
我们得到的图像看起来像 [图 7.5。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0208-02.jpg
图 7.6 进展的循环
TensorBoard (www.tensorflow.org/tensorboard) 是一个可以在本地运行的基于浏览器的应用程序。这是在训练过程中监视模型内部发生的一切的最佳方式。通过 TensorBoard,您可以
-
在训练过程中可视化监控指标
-
可视化您的模型架构
-
可视化激活和梯度的直方图
-
探索 3 维嵌入
如果您要监视的信息不仅仅是模型的最终损失,您可以更清晰地了解模型的工作情况,并更快地取得进展。使用 Keras 模型和 fit() 方法与 TensorBoard 最简单的方法是使用 callback_tensorboard()。在最简单的情况下,只需指定回调应写入日志的位置,就可以开始了:
model <- get_mnist_model()
model %>% compile(optimizer = “rmsprop”,
loss = “sparse_categorical_crossentropy”, metrics = “accuracy”)
model %>% fit(train_images, train_labels,
epochs = 10,
validation_data = list(val_images, val_labels),
callbacks = callback_tensorboard(log_dir = “logs/”))➊
➊ 日志目录的路径
一旦模型开始运行,它将在目标位置写入日志。然后,您可以通过调用 tensorboard() 查看日志;这将启动一个带有 tensorboard 运行的浏览器:
tensorboard(log_dir = “logs/”)➊
➊ 启动带有 TensorBoard 的浏览器
在 TensorBoard 界面中,您将能够监视您的训练和评估指标的实时图表(参见图 7.7)。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0209-01.jpg
图 7.7 TensorBoard 可用于轻松监控训练和评估指标。
7.4 编写您自己的训练和评估循环
fit() 工作流在易用性和灵活性之间取得了良好的平衡。这是你大部分时间将要使用的内容。然而,它并不意味着支持深度学习研究人员可能想做的一切,即使是使用自定义指标、自定义损失和自定义回调。
毕竟,内置的 fit() 工作流仅专注于 监督学习:一种设置,其中已知 目标(也称为 标签 或 注释)与您的输入数据相关联,并且您计算您的损失作为这些目标和模型预测的函数。然而,并非所有形式的机器学习都属于此类别。还有其他设置,其中没有明确的目标存在,例如 生成式学习(我们将在第十二章中讨论)、自监督学习(其中目标从输入中获取)和 强化学习(学习受偶尔“奖励”驱动,就像训练一只狗一样)。即使您正在进行常规的监督学习,作为研究人员,您可能也希望添加一些需要低级别灵活性的新颖功能。
每当您发现内置的 fit() 不足以应对某种情况时,您将需要编写自己的自定义训练逻辑。在第二章和第三章中,您已经看到了低级别训练循环的简单示例。作为提醒,典型训练循环的内容如下:
-
1 在梯度磁带中运行前向传播(计算模型的输出)以获取当前数据批次的损失值。
-
2 获取损失相对于模型权重的梯度。
-
3 更新模型的权重以降低当前数据批次的损失值。
这些步骤将根据需要重复执行多个批次。这本质上是 fit() 在内部执行的操作。在本节中,您将学习如何从头开始重新实现 fit(),这将为您提供编写任何可能出现的训练算法所需的所有知识。让我们详细看一下。
7.4.1 训练与推断
到目前为止,您已经看到了低级别训练循环示例,其中步骤 1(前向传播)通过 predictions <- model(inputs) 完成,步骤 2(获取梯度磁带计算的梯度)通过 gradients <- tape g r a d i e n t ( l o s s , m o d e l gradient(loss, model gradient(loss,modelweights) 完成。在一般情况下,实际上有两个您需要考虑的细微之处。
某些 Keras 层,如 layer_dropout(),在训练和推断(用于生成预测时)期间有不同的行为。这些层在其 call() 方法中公开了一个 training 布尔参数。调用 dropout(inputs, training = TRUE) 将会丢弃一些激活项,而调用 dropout(inputs, training = FALSE) 则不会做任何操作。顺延而言,Functional 和 Sequential 模型也在它们的 call() 方法中公开了这个 training 参数。记得在前向传播时传递 training = TRUE!因此,我们的前向传播变成了 predictions <- model(inputs, training = TRUE)。
另外,请注意,当您检索模型权重的梯度时,您不应该使用 tape g r a d i e n t s ( l o s s , m o d e l gradients(loss, model gradients(loss,modelweights),而应该使用 tape g r a d i e n t s ( l o s s , m o d e l gradients(loss, model gradients(loss,modeltrainable_weights)。实际上,层和模型拥有两种权重:
-
可训练的权重 —— 这些权重应通过反向传播来最小化模型的损失,例如 Dense 层的核和偏置。
-
不可训练的权重 —— 这些权重应由拥有它们的层在前向传播中更新。例如,如果您想要一个自定义层来保存到目前为止已处理了多少批次的计数器信息,那么该信息将存储在不可训练的权重中,并且在每个批次中,您的层将计数器递增一次。
在 Keras 内置层中,唯一具有不可训练权重的层是 layer_ batch_normalization(),我们将在第九章中讨论。批量归一化层需要不可训练权重来跟踪通过它的数据的平均值和标准差的信息,以便执行特征归一化的在线近似(这是您在第六章学到的概念)。考虑到这两个细节,监督学习的训练步骤最终看起来像这样:
library(tensorflow)
train_step <- function(inputs, targets) {
with(tf$GradientTape() %as% tape, {
predictions <- model(inputs, training = TRUE)
loss <- loss_fn(targets, predictions)
})
梯度 <- tape g r a d i e n t s ( l o s s , m o d e l gradients(loss, model gradients(loss,modeltrainable_weights)
optimizer a p p l y g r a d i e n t s ( z i p l i s t s ( g r a d i e n t s , m o d e l apply_gradients(zip_lists(gradients, model applygradients(ziplists(gradients,modeltrainable_weights))
}
➊ 我们在第二章介绍了 zip_lists()。
7.4.2 度量的低级用法
在低级训练循环中,您可能希望利用 Keras 度量(无论是自定义的还是内置的)。您已经了解了度量 API:只需为每个目标和预测的批次调用 update_state(y_true, y_pred),然后使用 result()来查询当前度量值:
metric <- metric_sparse_categorical_accuracy()
targets <- c(0, 1, 2)
predictions <- rbind(c(1, 0, 0),
c(0, 1, 0),
c(0, 0, 1))
metric$update_state(targets, predictions)
current_result <- metric$result()
sprintf(“result: %.2f”, as.array(current_result))➊
[1] “result: 1.00”
➊ as.array()将 Tensor 转换为 R 值
您可能还需要跟踪标量值的平均值,例如模型的损失。您可以通过 metric_mean()来实现:
values <- c(0, 1, 2, 3, 4)
mean_tracker <- metric_mean()
for (value in values)
mean_tracker$update_state(value)
sprintf(“Mean of values: %.2f”, as.array(mean_tracker$result()))
[1] “Mean of values: 2.00”
在想要重置当前结果时,请记得使用 metric$reset_state()(在训练时期的开始或评估的开始时)。
7.4.3 完整的训练和评估循环
让我们将前向传播、反向传播和指标跟踪结合到一个类似 fit()的训练步骤函数中,该函数接受一批数据和目标,并返回 fit()进度条将显示的日志。
清单 7.21 编写逐步训练循环:训练步骤函数
model <- get_mnist_model()
loss_fn <- loss_sparse_categorical_crossentropy()➊
optimizer <- optimizer_rmsprop()➋
metrics <- list(metric_sparse_categorical_accuracy())➌
loss_tracking_metric <- metric_mean()➍
train_step <- function(inputs, targets) {
with(tf$GradientTape() %as% tape, {
predictions <- model(inputs, training = TRUE)➎
loss <- loss_fn(targets, predictions)
})
gradients <- tape$gradient(loss,➏
model$trainable_weights)➏
optimizer$apply_gradients(zip_lists(gradients,
model$trainable_weights))
logs <- list()
for (metric in metrics) {➐
metric$update_state(targets, predictions)
logs[[metric n a m e ] ] < − m e t r i c name]] <- metric name]]<−metricresult()
}
loss_tracking_metric$update_state(loss)➑
logs l o s s < − l o s s t r a c k i n g m e t r i c loss <- loss_tracking_metric loss<−losstrackingmetricresult()
日志➒
}
➊ 准备损失函数。
➋ 准备优化器。
➌ 准备要监视的指标列表。
➍ 准备一个 metric_mean()跟踪器来记录平均损失。
➎ 运行前向传播。请注意,我们传递 training = TRUE。
➏ 运行反向传播。请注意,我们使用 model$trainable_weights。
➐ 跟踪指标。
➑ 跟踪损失平均值。
➒ 返回指标和损失的当前值。
我们需要在每个迭代开始时和运行评估之前重置指标的状态。下面是一个实用函数来完成这项工作。
清单 7.22 逐步编写训练循环:重置指标
reset_metrics <- function() {
for (指标 in 指标)
metric$reset_state()
loss_tracking_metric$reset_state()
}
现在我们可以布置完整的训练循环了。请注意,我们使用来自 tfdatasets 包的 TensorFlow 数据集对象,将我们的 R 数组数据转换为以大小为 32 的批次迭代的迭代器。机制与我们在第二章实施的数据集迭代器是相同的,只是现在的名称不同了。我们用 tensor_slices_dataset()从 R 数组构建 TensorFlow 数据集实例,用 as_iterator()将其转换为迭代器,然后重复调用 iter_next()来获取下一个批次。在第二章看到的不同之一是,iter_next()返回的是 Tensor 对象,而不是 R 数组。我们将在第八章更详细地介绍 tfdatasets。
清单 7.23 逐步编写训练循环:循环本身
library(tfdatasets)
training_dataset <-
list(inputs = train_images, targets = train_labels) %>%
tensor_slices_dataset() %>%
dataset_batch(32)
迭代周期 <- 3
for (迭代周期 in seq(epochs)) {
reset_metrics()
training_dataset_iterator <- as_iterator(training_dataset)
重复 {
batch <- iter_next(training_dataset_iterator)
if (is.null(batch))➊
中断
logs <- train_step(batch i n p u t s , b a t c h inputs, batch inputs,batchtargets)
}
writeLines(c(
sprintf(“第 %s 次迭代结束时的结果”, 迭代周期),
sprintf(“…%s: %.4f”, names(logs), sapply(logs, as.numeric))
))
}
第 1 次迭代结束时的结果
…稀疏分类准确率:0.9156
…损失:0.2687
第 2 次迭代结束时的结果
…稀疏分类准确率:0.9539
…损失:0.1659
第 3 次迭代结束时的结果
…稀疏分类准确率:0.9630
…损失:0.1371
➊ 迭代器已耗尽
这是评估循环:一个简单的 for 循环,它不断调用 test_step()函数,该函数处理单个数据批次。test_step()函数只是 train_step()逻辑的一个子集。它省略了处理更新模型权重的代码——也就是说,所有涉及 GradientTape()和优化器的代码。
清单 7.24 逐步编写评估循环
test_step <- function(inputs, targets) {
predictions <- model(inputs, training = FALSE)➊
loss <- loss_fn(targets, predictions)
logs <- list()
for (metric in metrics) {
metric$update_state(targets, predictions)
logs[[paste0(“val_”, metric n a m e ) ] ] < − m e t r i c name)]] <- metric name)]]<−metricresult()
}
loss_tracking_metric$update_state(loss)
logs[[“val_loss”]] <- loss_tracking_metric$result()
logs
}
val_dataset <- list(val_images, val_labels) %>%
tensor_slices_dataset() %>%
dataset_batch(32)
reset_metrics()
val_dataset_iterator <- as_iterator(val_dataset)
repeat {
batch <- iter_next(val_dataset_iterator)
if(is.null(batch)) break➋
c(inputs_batch, targets_batch) %<-% batch
logs <- test_step(inputs_batch, targets_batch)
}
writeLines(c(
“评估结果:”,
sprintf(“…%s: %.4f”, names(logs), sapply(logs, as.numeric))
))
评估结果:
…val_sparse_categorical_accuracy: 0.9461
…val_loss: 0.1871
➊ 请注意我们将 training 参数设置为 FALSE。
➋ 一旦数据集批处理迭代器耗尽,iter_next() 将返回 NULL。
恭喜你——你刚刚重新实现了 fit() 和 evaluate()!或者几乎实现了:fit() 和 evaluate() 支持更多功能,包括大规模分布式计算,这需要更多的工作。它还包括几个关键的性能优化。让我们看看其中一个优化:TensorFlow 函数编译。
7.4.4 使用 tf_function() 提高性能
你可能已经注意到,尽管实现了基本相同的逻辑,但自定义循环的运行速度明显比内置的 fit() 和 evaluate() 慢得多。这是因为,默认情况下,TensorFlow 代码是逐行执行的,即时执行,就像使用 R 数组的常规 R 代码一样。即时执行使得调试代码更容易,但从性能的角度来看远非最佳。
将你的 TensorFlow 代码编译成一个可以进行全局优化的 计算图 比逐行解释代码更高效。要做到这一点的语法非常简单:只需在执行之前对你想要编译的任何函数调用 tf_function(),就像下面的示例中所示。
示例 7.25 使用 tf_function() 与我们的评估步骤函数
tf_test_step <- tf_function(test_step)➊
val_dataset_iterator <- as_iterator(val_dataset)➋
reset_metrics()
while(!is.null(iter_next(val_dataset_iterator) -> batch)) {
c(inputs_batch, targets_batch) %<-% batch
logs <- tf_test_step(inputs_batch, targets_batch)➌
}
writeLines(c(
“评估结果:”,
sprintf(“…%s: %.4f”, names(logs), sapply(logs, as.numeric))
))
评估结果:
…val_sparse_categorical_accuracy: 0.5190
…val_loss: 1.6764
➊ 将我们之前定义的 test_step 传递给 tf_function()。
➋ 重用前一个示例中定义的相同 TF 数据集,但创建一个新的迭代器。
➌ 这次使用编译后的测试步骤函数。
在我的机器上,我们的评估循环运行时间从 2.4 秒缩短到只有 0.6 秒。快得多!
当 TF 数据集迭代循环也被编译为图操作时,速度提升甚至更大。你可以像这样使用 tf_function() 编译整个评估循环:
my_evaluate <- tf_function(function(model, dataset) {
reset_metrics()
for (batch in dataset) {
c(inputs_batch, targets_batch) %<-% batch
logs <- test_step(inputs_batch, targets_batch)
}
logs
})
system.time(my_evaluate(model, val_dataset))[“elapsed”]
elapsed
0.283
这进一步缩短了评估时间!
请记住,当您调试代码时,最好不要调用 tf_function(),而是急切地运行它。这样更容易跟踪错误。一旦您的代码可行并且想要使其快速,就可以向您的训练步骤和评估步骤或任何其他性能关键的函数添加 tf_function()修饰符。
7.4.5 利用 fit()进行自定义训练循环
在之前的章节中,我们完全从头编写了自己的训练循环。这样做为您提供了最大的灵活性,但同时您需要编写大量的代码,同时错过了许多方便的 fit()功能,例如回调或分布式训练的内置支持。
如果你需要自定义训练算法,但仍想利用内置的 Keras 训练逻辑的优势,那么在 fit()和从头编写训练循环之间实际上有一种中间状态:你可以提供自定义训练步骤功能,让框架完成其他工作。
您可以通过覆盖 Model 类的 train_step()方法来实现这一点。这是由 fit()用于每个数据批次调用的函数。然后您将能够像通常一样调用 fit(),它将在底层运行您自己的学习算法。下面是一个简单的例子:
-
我们通过调用 new_model_class()来创建一个子类 Model 的新类。
-
我们覆盖 train_step(data)方法。它的内容几乎与我们在前一节中使用的内容相同。它返回将指标名称(包括损失)映射到其当前值的命名列表。
-
我们实现了一个跟踪模型的 Metric 实例的 metrics 活动属性(active property)。这使得模型能够在每个纪元(epoch)和在调用 evaluate()时自动调用模型的 metrics 的 reset_state(),因此不必手动执行。
loss_fn <- loss_sparse_categorical_crossentropy()
loss_tracker <- metric_mean(name = “loss”)➊
CustomModel <- new_model_class(
classname = “CustomModel”,
train_step = function(data) {➋
c(inputs, targets) %<-% data
with(tf$GradientTape() %as% tape, {
predictions <- self(inputs, training = TRUE)➌
loss <- loss_fn(targets, predictions)
})
gradients <- tape g r a d i e n t ( l o s s , m o d e l gradient(loss, model gradient(loss,modeltrainable_weights)
optimizer a p p l y g r a d i e n t s ( z i p l i s t s ( g r a d i e n t s , m o d e l apply_gradients(zip_lists(gradients, model applygradients(ziplists(gradients,modeltrainable_weights))
loss_tracker$update_state(loss)➍
list(loss = loss_tracker$result())➎
},
metrics = mark_active(function() list(loss_tracker))➏
)
➊ 该度量对象将用于跟踪训练和评估期间每个批次损失的平均值。
➋ 我们覆盖 train_step 方法。
➌ 我们使用 self(inputs, training = TRUE)而不是 model(inputs, training = TRUE),因为我们的模型是类本身。
➍ 我们更新损失跟踪器指标,该指标跟踪损失的平均值。
➎ 通过查询损失跟踪器指标返回到目前为止的平均损失。
➏ 您想在跨时期重置的任何指标都应在此处列出。
现在,我们可以实例化我们的自定义模型,编译它(我们仅传递优化器,因为损失已在模型外部定义),然后像往常一样使用 fit() 进行训练:
inputs <- layer_input(shape = c(28 * 28))
features <- inputs %>%
layer_dense(512, activation = “relu”) %>%
layer_dropout(0.5)
outputs <- features %>%
layer_dense(10, activation = “softmax”)
model <- CustomModel(inputs = inputs, outputs = outputs)➊
model %>% compile(optimizer = optimizer_rmsprop())
model %>% fit(train_images, train_labels, epochs = 3)
➊ 因为我们没有提供 initialize() 方法,所以使用与 keras_model() 相同的签名:inputs、outputs 和可选的 name。
有几个要点需要注意:
-
这种模式不会阻止您使用功能 API 构建模型。 无论您是构建序贯模型、功能 API 模型还是子类模型,都可以这样做。
-
当您重写 train_step 时,您不需要调用 tf_function() ——框架会为您执行此操作。
那么,指标呢? 以及如何通过 compile() 配置损失? 在调用 compile() 之后,您可以访问以下内容:
-
self$compiled_loss—您传递给 compile() 的损失函数。
-
self c o m p i l e d m e t r i c s —一个包装器,用于您传递的指标列表,它允许您调用 s e l f compiled_metrics—一个包装器,用于您传递的指标列表,它允许您调用 self compiledmetrics—一个包装器,用于您传递的指标列表,它允许您调用selfcompiled_metrics$update_state() 一次更新所有指标。
-
self$metrics—您传递给 compile() 的实际指标列表。 请注意,它还包括一个跟踪损失的指标,类似于我们之前手动进行的 loss_tracking_metric。
因此,我们可以这样写:
CustomModel <- new_model_class(
classname = “CustomModel”,
train_step = function(data) {
c(inputs, targets) %<-% data
with(tf$GradientTape() %as% tape, {
预测 <- self(inputs, training = TRUE)
loss <- self$compiled_loss(targets, predictions)➊
})
梯度 <- tape g r a d i e n t ( l o s s , m o d e l gradient(loss, model gradient(loss,modeltrainable_weights)
optimizer a p p l y g r a d i e n t s ( z i p l i s t s ( g r a d i e n t s , m o d e l apply_gradients(zip_lists(gradients, model applygradients(ziplists(gradients,modeltrainable_weights))
self c o m p i l e d m e t r i c s compiled_metrics compiledmetricsupdate_state(
targets, predictions)➋
results <- list()
for(metric in self$metrics)
results[[metric n a m e ] ] < − m e t r i c name]] <- metric name]]<−metricresult()
results➌
}
)
➊ 通过 self$compiled_loss 计算损失。
➋ 通过 self$compiled_metrics 更新模型的指标。
➌ 返回一个命名列表,将指标名称映射到其当前值。
让我们试一试:
inputs <- layer_input(shape = c(28 * 28))
特征 <- inputs %>%
layer_dense(512, activation = “relu”) %>%
layer_dropout(0.5)
outputs <- features %>% layer_dense(10, activation = “softmax”)
model <- CustomModel(inputs = inputs, outputs = outputs)
model %>% compile(optimizer = optimizer_rmsprop(),
loss = loss_sparse_categorical_crossentropy(),
metrics = metric_sparse_categorical_accuracy())
model %>% fit(train_images, train_labels, epochs = 3)
那是大量的信息,但现在你已经了解足够的内容,可以使用 Keras 几乎做任何事情。
总结
-
Keras 提供了一系列不同的工作流程,基于逐步透露复杂性的原则。它们都能够平稳地协同运作。
-
你可以通过 Sequential API 的 keras_model_sequential(),通过 Functional API 的 keras_model(),或者通过子类化 Model 类的 new_model_class()来构建模型。大多数情况下,你将使用 Functional API。
-
训练和评估模型的最简单方法是通过默认的 fit()和 evaluate()方法。
-
Keras 回调提供了一种简单的方法,在调用 fit()期间监视模型并根据模型状态自动采取行动。
-
你还可以通过覆盖 train_ step()方法完全控制 fit()的行为。
-
除了 fit()之外,你还可以完全从头开始编写自己的训练循环。这对于实现全新训练算法的研究人员非常有用。
第八章:计算机视觉的深度学习介绍
本章内容包括
-
理解卷积神经网络(卷积网络)
-
使用数据增强来减轻过拟合
-
使用预训练的卷积网络进行特征提取
-
对预训练的卷积网络进行微调
计算机视觉是深度学习的最早和最大的成功故事。每天,您都在通过 Google 照片、Google 图像搜索、YouTube、相机应用中的视频滤镜、OCR 软件等与深度视觉模型进行交互。这些模型还是自动驾驶、机器人技术、AI 辅助医学诊断、自动零售结账系统甚至自动农业等尖端研究的核心。
计算机视觉是在 2011 年至 2015 年间深度学习初期崛起的问题领域。一种称为卷积神经网络的深度学习模型开始在那个时候在图像分类竞赛中取得了非常好的成绩,首先是丹·西雷赛安(Dan Ciresan)在两个小众竞赛中获胜(ICDAR 2011 年中文字符识别竞赛和 IJCNN 2011 年德国交通标志识别竞赛),然后在 2012 年秋季更为显著,辛顿(Hinton)的团队赢得了备受瞩目的 ImageNet 大规模视觉识别挑战赛。很快,许多更有前景的成果开始涌现在其他计算机视觉任务中。
有趣的是,这些早期的成功并没有让深度学习在当时成为主流——这需要几年的时间。计算机视觉研究界花了很多年投资于除了神经网络之外的方法,它们并不完全准备放弃它们,只因为有了新的玩家。在 2013 年和 2014 年,深度学习仍然面临着来自许多资深计算机视觉研究人员的激烈质疑。直到 2016 年,它才最终占据主导地位。我(弗朗索瓦)还记得在 2014 年 2 月劝告我的一位前教授转向深度学习。“这是下一个大事!”我会说。“嗯,也许它只是一时的热门话题,”他回答道。到了 2016 年,他的整个实验室都在进行深度学习。一个时代到来的理念是无法阻挡的。
本章介绍了卷积神经网络,也称为卷积网络,这种类型的深度学习模型现在几乎在计算机视觉应用中被普遍使用。您将学习将卷积网络应用于图像分类问题,特别是那些涉及小训练数据集的问题,如果您不是一家大型技术公司,则这是最常见的用例。
8.1 卷积网络简介
我们即将深入探讨卷积网络是什么以及为什么它们在计算机视觉任务中取得如此成功的理论。但首先,让我们以一个简单的卷积网络示例来实际了解一下,该示例对 MNIST 数字进行分类,这是我们在第二章中使用全连接网络执行的任务(当时我们的测试准确率为 97.8%)。尽管卷积网络将是基本的,但其准确性将远远超出我们第二章中的全连接模型。
以下列表显示了一个基本卷积神经网络的外观。它是一堆layer_conv_2d()和layer_max_pooling_2d()层。你很快就会明白它们的作用。我们将使用我们在上一章介绍的 Functional API 来构建模型。
inputs <— layer_input(shape = c(28, 28, 1))
outputs <— inputs %>%
layer_conv_2d(filters = 32, kernel_size = 3, activation = "relu") %>%
layer_max_pooling_2d(pool_size = 2) %>%
layer_conv_2d(filters = 64, kernel_size = 3, activation = "relu") %>%
layer_max_pooling_2d(pool_size = 2) %>%
layer_conv_2d(filters = 128, kernel_size = 3, activation = "relu") %>%
layer_flatten() %>% layer_dense(10, activation = "softmax")
model <— keras_model(inputs, outputs)
重要的是,卷积神经网络的输入是形状为(图像高度、图像宽度、图像通道数)的张量,不包括批处理维度。在这种情况下,我们将配置卷积神经网络以处理大小为(28、28、1)的输入,这是 MNIST 图像的格式。
让我们显示我们卷积神经网络的结构。
model
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0222-01.jpg
你可以看到每个 Conv2D 和 MaxPooling2D 层的输出都是形状为(高度,宽度,通道数)的秩为 3 的张量。随着模型的加深,宽度和高度维度会变小。通道的数量由传递给layer_conv_2d()层的第一个参数(32、64 或 128)来控制。
在最后一个 Conv2D 层之后,我们得到形状为(3、3、128)的输出,即一个 3 × 3 的具有 128 个通道的特征图。下一步是将此输出馈送到一个密集连接的分类器中,就像您已经熟悉的那样:一堆 Dense 层。这些分类器处理向量,这是 1D 的,而当前的输出是一个秩为 3 的张量。为了弥合差距,我们使用 Flatten 层将 3D 输出展平为 1D,然后再添加 Dense 层。最后,我们进行 10 分类,因此我们的最后一层有 10 个输出和 softmax 激活。
现在,让我们在 MNIST 数字上训练卷积神经网络。我们将重用第二章中 MNIST 示例中的大量代码。因为我们要进行 10 分类,并带有 softmax 输出,所以我们将使用分类交叉熵损失,因为我们的标签是整数,所以我们将使用稀疏版本的稀疏分类交叉熵。
c(c(train_images, train_labels), c(test_images, test_labels)) %<—%
dataset_mnist()
train_images <— array_reshape(train_images, c(60000, 28, 28, 1)) / 255
test_images <— array_reshape(test_images, c(10000, 28, 28, 1)) / 255
model %>% compile(optimizer = "rmsprop",
loss = "sparse_categorical_crossentropy"
metrics = c("accuracy"))
model %>% fit(train_images, train_labels, epochs = 5, batch_size = 64)
让我们在测试数据上评估模型。
result <— evaluate(model, test_images, test_labels)
cat("测试准确率:", result['accuracy'], "\n")
测试准确率:0.9915
而第二章中的密集连接模型的测试准确率为 97.8%,基本卷积神经网络的测试准确率为 99.1%:我们将错误率减少了约 60%(相对)。不错!
为什么这个简单的卷积神经网络效果这么好,相比之下,与密集连接模型相比如此?为了回答这个问题,让我们深入了解一下 Conv2D 和 MaxPooling2D 层的作用。
8.1.1 卷积操作
密集连接层和卷积层之间的根本区别在于:密集层学习其输入特征空间中的全局模式(例如,对于 MNIST 数字,涉及所有像素的模式),而卷积层学习局部模式——在图像的情况下,输入的小 2D 窗口中发现的模式(参见图 8.1)。在先前的示例中,这些窗口都是 3×3。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0223-01.jpg
图 8.1 图像可以被分解为边缘、纹理等局部模式。
这一关键特性赋予了卷积神经网络两个有趣的特性:
-
它们学习的模式是平移不变的——在图片的右下角学习了某种模式后,卷积神经网络可以在任何地方识别它——例如,在左上角。密集连接模型如果出现在新位置,就必须重新学习该模式。这使得处理图像时,卷积神经网络在数据效率上更具优势(因为视觉世界在本质上是平移不变的):它们需要更少的训练样本来学习具有泛化能力的表示。
-
它们可以学习空间模式的层次结构——第一个卷积层将学习小的局部模式,例如边缘,第二个卷积层将学习由第一层特征组成的较大模式,依此类推(参见图 8.2)。这使得卷积神经网络能够高效地学习越来越复杂和抽象的视觉概念,因为视觉世界在本质上是空间层次结构。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0224-01.jpg
图 8.2 视觉世界形成了视觉模块的空间层次结构:基本线条或纹理结合成简单对象,如眼睛或耳朵,它们又结合成“猫”等高级概念。
卷积操作在称为特征图的三阶张量上进行,具有两个空间轴(高度和宽度)以及一个深度轴(也称为通道轴)。对于 RGB 图像,深度轴的维数为 3,因为图像具有三个颜色通道:红色、绿色和蓝色。对于黑白图片,如 MNIST 数字,深度为 1(灰度级)。卷积操作从其输入特征图中提取补丁,并对所有这些补丁应用相同的变换,生成一个输出特征图。此输出特征图仍然是一个三阶张量:它具有宽度和高度。它的深度可以是任意的,因为输出深度是层的一个参数,该深度轴中的不同通道不再代表 RGB 输入中的特定颜色;相反,它们代表滤波器。滤波器编码输入数据的特定方面:在高层次上,单个滤波器可以编码“输入中存在面部”的概念,例如。
在 MNIST 示例中,第一卷积层接受尺寸为 (28, 28, 1) 的特征图,并输出尺寸为 (26, 26, 32) 的特征图:它在其输入上计算 32 个滤波器。这 32 个输出通道中的每一个都包含一个 26 × 26 的值网格,这是该滤波器在输入上的响应图,指示了该滤波器模式在输入的不同位置的响应(参见图 8.3)。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0225-01.jpg
图 8.3 响应图的概念:输入中不同位置的模式的 2D 地图
这就是术语特征图的含义:深度轴中的每个维度都是一个特征(或滤波器),而二阶张量输出[, , n] 是此滤波器在输入上的 2D 空间响应图。
卷积由两个关键参数定义:
-
从输入中提取的补丁的大小 —— 这些通常是 3 × 3 或 5 × 5。在示例中,它们是 3 × 3,这是一个常见的选择。
-
输出特征图的深度 —— 这是由卷积计算的滤波器数量。示例从深度为 32 开始,最终达到了深度为 64。
在 layer_conv_2d() 中,这些参数是传递给层的第一个参数(与输入组合的参数之后):inputs %>% layer_conv_2d(output_depth, c(window_ height, window_width))。
卷积的工作原理是通过滑动这些大小为 3 × 3 或 5 × 5 的窗口,遍历 3D 输入特征图,停留在每个可能的位置,并提取周围特征的 3D 补丁(形状为 (window_height, window_width, input_depth))。然后将每个这样的 3D 补丁转化为形状为 (output_depth) 的 1D 向量,通过与一个称为卷积核的学习权重矩阵进行张量积操作——同一个卷积核会在每个补丁中重复使用。所有这些向量(每个补丁一个)然后会在空间上重新组合成一个形状为 (height, width, output_depth) 的 3D 输出图。在输出特征图的每个空间位置都对应于输入特征图的相同位置(例如,输出的右下角包含了输入的右下角的信息)。例如,使用 3 × 3 窗口,向量 output[i, j, ] 来自于 3D 补丁 input[(i-1):(i+1), (j-1):(j+1), ]。完整的过程详见图 8.4。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0226-01.jpg
图 8.4 卷积的工作原理。
请注意,输出的宽度和高度可能与输入的宽度和高度不同,原因有两个:
-
边界效应可以通过对输入特征图进行填充来抵消。
-
我将在一秒钟内定义步幅的使用。
让我们更深入地了解这些概念。
理解边界效应和填充
考虑一个 5 × 5 的特征图(总共 25 个格子)。你只能在其中 9 个格子周围居中 3 × 3 窗口,形成一个 3 × 3 的网格(见 图 8.5)。因此,输出特征图将是 3 × 3。它会略微缩小:在这种情况下,每个维度正好缩小两个格子。在早期的例子中,你可以看到这种边界效应:开始时输入是 28 × 28,在第一卷积层之后变成了 26 × 26。
如果您想获得与输入相同空间维度的输出特征图,您可以使用填充。填充包括在输入特征图的每一侧添加适当数量的行和列,以便在每个输入格子周围可以放置中心卷积窗口。对于 3 × 3 窗口,您需要在右边添加一列,左边添加一列,顶部添加一行,底部添加一行。对于 5 × 5 窗口,您需要添加两行(详见 图 8.6)。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0227-01.jpg
图 8.5 在 5 × 5 输入特征图中 3 × 3 补丁的有效位置
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0227-02.jpg
图 8.6 对 5 × 5 输入进行填充,以能够提取 25 个 3 × 3 补丁
在 layer_conv_2d() 中,填充可通过 padding 参数进行配置,padding 参数可以取两个值:“valid”,表示不进行填充(只使用有效的窗口位置),以及“same”,表示“以这样一种方式填充,使得输出的宽度和高度与输入相同。” padding 参数的默认值是“valid”。
理解卷积步幅
可以影响输出大小的另一个因素是步幅的概念。到目前为止,我们对卷积的描述假定卷积窗口的中心瓦片都是连续的。但是,两个连续窗口之间的距离是卷积的一个参数,称为其步幅,默认为 1。可以进行步进卷积:步幅大于 1 的卷积。在图 8.7 中,您可以看到在没有填充的情况下,3×3 卷积以步幅 2 在 5×5 输入上提取的补丁。
使用步长 2 意味着特征映射的宽度和高度被下采样了 2 倍(除了由边界效应引起的任何变化)。步进卷积在分类模型中很少使用,但对于某些类型的模型非常方便,你将在下一章中看到。
在分类模型中,我们倾向于使用最大池化操作来对特征映射进行下采样,你在我们的第一个卷积神经网络示例中看到了它的作用。让我们更深入地研究一下。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0228-01.jpg
图 8.7 3 × 3 卷积补丁,步长为 2 × 2
8.1.2 最大池化操作
在卷积神经网络示例中,您可能已经注意到,在每个 layer_max_pooling_2d()之后,特征映射的大小都会减半。例如,在第一个 layer_max_pooling_2d()之前,特征映射为 26×26,但最大池化操作将其减半为 13×13。这就是最大池化的作用:对特征映射进行积极地下采样,就像步进卷积一样。
最大池化包括从输入特征映射中提取窗口,并输出每个通道的最大值。概念上类似于卷积,不同之处在于,不是通过学习的线性转换(卷积核)来转换局部补丁,而是通过硬编码的最大张量操作来转换它们。与卷积的一个重大区别是,最大池化通常使用 2×2 的窗口和步长 2 来进行,以将特征映射下采样 2 倍。另一方面,卷积通常使用 3×3 的窗口和无步长(步长为 1)进行。
为什么要以这种方式下采样特征映射?为什么不删除最大池化层,并一直保持相当大的特征映射?让我们看看这个选择。我们的模型将如下列表所示。
列表 8.5 列表 8.5 缺少最大池化层的结构不正确的卷积神经网络
inputs <— layer_input(shape = c(28, 28, 1))
outputs <— inputs %>%
layer_conv_2d(filters = 32, kernel_size = 3, activation = “relu”) %>%
layer_conv_2d(filters = 64, kernel_size = 3, activation = “relu”) %>%
layer_conv_2d(filters = 128, kernel_size = 3, activation = “relu”) %>%
layer_flatten() %>%
layer_dense(10, activation = “softmax”)
model_no_max_pool <— keras_model(inputs = inputs, outputs = outputs)
这是模型的摘要:
model_no_max_pool
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0229-01.jpg
这个设置有什么问题?有两个问题:
-
它不利于学习空间特征的层次结构。第三层中的 3×3 窗口仅包含来自最初输入的 7×7 窗口的信息。卷积网络学习的高级模式与初始输入相比仍然非常小,这可能不足以学习分类数字(尝试通过仅使用 7×7 像素的窗口查看数字来识别数字!)。我们需要来自前一个卷积层的特征包含关于整个输入的信息。
-
最终的特征映射每个样本有 22×22×128=61,952 个系数
简而言之,使用下采样的原因是减少要处理的特征图系数的数量,并通过使连续的卷积层查看越来越大的窗口(就涵盖原始输入的部分而言)来引导空间滤波器层次结构。
请注意,最大池化并不是您可以实现这种下采样的唯一方法。正如您已经知道的那样,您还可以在之前的卷积层中使用步长。您还可以使用平均池化,而不是最大池化,其中每个本地输入补丁通过在补丁上每个通道的平均值进行变换,而不是最大值。但是,最大池化比这些替代解决方案更有效。原因是特征倾向于在特征地图的不同瓷砖上编码某些模式或概念的空间存在(因此是特征图一词),查看不同特征的最大存在而不是平均存在更具信息性。最合理的子采样策略是首先通过未简化的卷积产生密集的特征映射,然后查看特征在小补丁上的最大激活,而不是查看输入的更稀疏的窗口(通过分步卷积)或平均输入补丁,这可能导致您错过或稀释特征存在信息。
现在,您应该了解卷积神经网络的基础知识-特征图、卷积和最大池化-并且应该知道如何构建一个小型卷积神经网络来解决玩具问题,如 MNIST 数字分类。现在让我们继续探讨更有用,实用的应用。
8.2 从头开始在小数据集上训练 convnet
使用很少的数据来训练图像分类模型是一种常见情况,在实践中,如果您在专业上进行计算机视觉,可能会遇到这种情况。“一些”样本可以从几百个到几万个图像不等。作为实际示例,我们将专注于将图像分类为猫或狗的元素,该数据集包含 5,000 张猫和狗的图片(2,500 只猫,2,500 只狗)。我们将使用 2,000 张图片进行训练,1,000 张用于验证,2,000 张用于测试。
在本节中,我们将回顾一种解决这个问题的基本策略:使用很少的数据从头开始训练一个新模型。我们将首先使用没有正则化的小型卷积神经网络对 2000 个训练样本进行简单训练,以建立一个基线模型来评估可达到的效果。这将使我们的分类准确性达到大约 70%。此时,主要问题在于过拟合。然后,我们将介绍数据增强——一种处理计算机视觉中的过拟合的强大技术。通过使用数据增强,我们将改善模型,使其准确率提高到 80-85%。
在下一节中,我们将回顾应用深度学习于小数据集的另外两种关键技术:使用预训练模型进行特征提取(将准确率提高到 97.5%)和微调预训练模型(将准确率提高到最终的 98.5%)。这三种战略——从头开始训练小模型、使用预训练模型进行特征提取,以及微调预训练模型——将为您应对小数据集的图像分类问题提供工具箱。
8.2.1 深度学习在小数据问题中的相关性。
训练模型所需的“足够样本”是相对的——首先相对于你要训练的模型的规模和深度。使用仅有几十个样本训练卷积神经网络以解决复杂问题是不可能的,但是对于小型、规范良好的模型和简单任务,数百个样本可能足够了。因为卷积神经网络学习局部、平移不变的特征,它们在感知性问题上非常数据高效。即使在非常小的图像数据集上从头开始训练卷积神经网络,也可以产生合理的结果,而不需要进行任何自定义的特征工程。你将在本节中看到这一点的实践演示。
此外,深度学习模型具有高度重用性的本质特点:你可以使用一种基于大规模数据集训练的图像分类或语音转文本模型,并在只做出微小修改的情况下将其应用于完全不同的问题。尤其是在计算机视觉领域,很多预训练模型(通常在 ImageNet 数据集上训练)现在已经公开提供下载,可以使用非常少量的数据来引导强大的视觉模型。这是深度学习的最大优势之一:特征重用。你将在下一节中详细了解这一点。首先,我们需要开始处理数据。
8.2.2 下载数据
我们将使用的 Dogs vs. Cats 数据集并不随 Keras 打包。这是 Kaggle 在 2013 年末作为计算机视觉竞赛的一部分提供的,当时 convnets 还不是主流。您可以从 www.kaggle.com/c/dogs-vs-cats/data 下载原始数据集(如果您还没有 Kaggle 帐户,您需要创建一个——别担心,这个过程很简单)。您也可以使用 Kaggle 命令行 API 下载数据集。
下载 Kaggle 数据集
Kaggle 提供了一个易于使用的 API 来以编程方式下载托管在 Kaggle 上的数据集。您可以使用它将 Dogs vs. Cats 数据集下载到您的本地计算机。例如,通过在 R 中运行单个命令,就可以轻松下载此数据集。
但是,API 的访问权限受限于 Kaggle 用户,因此要运行上述命令,您首先需要进行身份验证。kaggle 包将在位于 ~/.kaggle/kaggle.json 的 JSON 文件中查找您的登录凭据。让我们创建这个文件。
首先,您需要创建一个 Kaggle API 密钥并将其下载到本地计算机。只需在 web 浏览器中导航到 Kaggle 网站,登录,然后转到“我的帐户”页面。在您的帐户设置中,您会找到一个 API 部分。点击“创建新的 API 令牌”按钮将生成一个名为 kaggle.json 的密钥文件,并将其下载到您的计算机上。
最后,创建一个 ~/.kaggle 文件夹。作为安全最佳实践,您还应确保该文件仅由当前用户自己可读(仅适用于 Mac 或 Linux,而不是 Windows)。
因为在接下来的章节中我们将执行大量的文件系统操作,我们将使用 fs R 包,它比基本的 R 文件系统函数更易于使用。(您可以通过 install.packages(“fs”) 从 CRAN 安装它。)
准备 Kaggle API 密钥:
library(fs)
dir_create(“~/.kaggle”)
file_move(“~/Downloads/kaggle.json”, “~/.kaggle/”)
file_chmod(“~/.kaggle/kaggle.json”, “0600”)➊
➊ 将文件标记为仅自己可读
通过 pip 安装 kaggle 包:
reticulate::py_install(“kaggle”, pip = TRUE)
现在,您可以下载我们即将使用的数据:
system(‘kaggle competitions download -c dogs-vs-cats’)
第一次尝试下载数据时,可能会出现“403 Forbidden”错误。这是因为在下载之前,您需要接受与数据集相关的条款——您需要登录 Kaggle 帐户并点击“我理解并接受”按钮,网址为 www.kaggle.com/c/dogs-vs-cats/rules。您只需要这样做一次。
最后,数据以压缩的 zip 文件 dogs-vs-cats.zip 的形式下载。该 zip 文件本身包含另一个压缩的 zip 文件 train.zip,这是我们将要使用的训练数据。我们使用 zip R 包(可以通过 install.packages(“zip”) 从 CRAN 安装)将 train.zip 解压缩到一个新目录 dogs-vs-cats 中:
zip::unzip(‘dogs-vs-cats.zip’, exdir = “dogs-vs-cats”, files = “train.zip”)
zip::unzip(“dogs-vs-cats/train.zip”, exdir = “dogs-vs-cats”)
我们数据集中的图片是中等分辨率的彩色 JPEG。图 8.8 展示了一些示例。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0232-01.jpg
图 8.8 狗与猫数据集的样本。大小未经修改:样本的大小、颜色、背景等各不相同。
毫不奇怪,最早在 2013 年的狗与猫 Kaggle 竞赛中,获胜者都是使用了卷积神经网络。最好的参赛作品达到了 95% 的准确率。在这个示例中,我们将会在下一节中实现接近这个准确率,尽管我们将只使用竞争对手可用数据的不到 10% 进行模型训练。
这个数据集包含 25,000 张狗和猫的图片(每类别 12,500 张),大小为 543 MB(压缩后)。下载并解压缩数据后,我们将创建一个新数据集,其中包含三个子集:一个包含每个类别 1,000 个样本的训练集,一个包含每个类别 500 个样本的验证集,以及一个包含每个类别 1,000 个样本的测试集。为什么这样做?因为你在职业生涯中遇到的许多图像数据集只包含几千个样本,而不是几万个样本。有更多的数据可用会使问题变得更容易,因此使用小数据集进行学习是一个好的做法。
我们将要处理的子采样数据集将具有以下目录结构:
cats_vs_dogs_small/
…train/
……cat/➊
……dog/➋
…validation/
……cat/➌
……dog/➍
…test/
……cat/➎
……dog/➏
➊ 包含 1,000 张猫的图片
➋ 包含 1,000 张狗的图片
➌包含 500 张猫的图片
➍包含 500 张狗的图片
➎ 包含 1,000 张猫的图片
➏包含 1,000 张狗的图片
让我们通过几次调用 {fs} 函数来实现这一点。
列表 8.6 将图片复制到训练、验证和测试目录
library(fs)
original_dir <— path(“dogs-vs-cats/train”)➊
new_base_dir <— path(“cats_vs_dogs_small”)➋
make_subset <— function(subset_name,➌
start_index, end_index) {
for (category in c(“dog”, “cat”)) {
file_name <— glue::glue(“{category}.{ start_index:end_index }.jpg”)
dir_create(new_base_dir / subset_name / category)
file_copy(original_dir / file_name,
new_base_dir / subset_name / category / file_name)
}
}
make_subset(“train”, start_index = 1, end_index = 1000)➍
make_subset(“validation”, start_index = 1001, end_index = 1500)➎
make_subset(“test”, start_index = 1501, end_index = 2500)➏
➊ 原始数据集解压缩后的目录路径
➋ 我们将存储较小数据集的目录
➌ 将猫和狗图片在开始索引和结束索引之间复制到子目录 new_base_dir/{subset_name}/cat(和/dog)的实用函数。“subset_name” 将是 “train”、“validation” 或 “test” 中的一个。
➍ 创建训练子集,包含每类别的前 1,000 张图片。
➎ 创建验证子集,包含每类别的接下来 500 张图片。
➏ 用每个类别的接下来的 1,000 张图像创建测试子集。
现在我们有 2,000 张训练图像,1,000 张验证图像和 2,000 张测试图像。每个数据集包含相同数量的来自每个类别的样本:这是一个平衡的二元分类问题,这意味着分类准确度将是一个适当的成功度量。
8.2.3 构建模型
我们将重复使用你在第一个示例中看到的相同的通用模型结构:卷积网络将是交替的 layer_conv_2d()(使用 relu 激活)和 layer_ max_pooling_2d() 层的堆叠。
但因为我们处理的是更大的图像和更复杂的问题,我们将相应地使我们的模型更大:它将具有两个更多的 layer_conv_2d() 和 layer_max_pooling_2d() 阶段。这既增加了模型的容量,又进一步减小了特征图的大小,使得当我们到达 layer_flatten() 时它们不会过大。在这里,因为我们从尺寸为 180 像素 × 180 像素的输入开始(一个相对随意的选择),所以在 layer_flatten() 之前我们得到大小为 7 × 7 的特征图。
特征图的深度在模型中逐渐增加(从 32 增加到 256),而特征图的大小逐渐减小(从 180 × 180 减小到 7 × 7)。这是你几乎在所有卷积网络中都会看到的模式。
因为我们正在处理一个二元分类问题,所以我们将模型结束于一个单元(大小为 1 的 layer_dense())和一个 sigmoid 激活。这个单元将编码模型正在观察的一个类别或另一个类别的概率。
最后一个小差异:我们将以一个 layer_rescaling() 开始模型,它将重新缩放图像输入(其值最初在 [0, 255] 范围内)到 [0, 1] 范围内。
列表 8.7 实例化用于狗与猫分类的小型卷积网络
inputs <— layer_input(shape = c(180, 180, 3))➊
outputs <— inputs %>%
layer_rescaling(1 / 255) %>%➋
layer_conv_2d(filters = 32, kernel_size = 3, activation = “relu”) %>%
layer_max_pooling_2d(pool_size = 2) %>%
layer_conv_2d(filters = 64, kernel_size = 3, activation = “relu”) %>%
layer_max_pooling_2d(pool_size = 2) %>%
layer_conv_2d(filters = 128, kernel_size = 3, activation = “relu”) %>%
layer_max_pooling_2d(pool_size = 2) %>%
layer_conv_2d(filters = 256, kernel_size = 3, activation = “relu”) %>%
layer_max_pooling_2d(pool_size = 2) %>%
layer_conv_2d(filters = 256, kernel_size = 3, activation = “relu”) %>% layer_flatten() %>%
layer_dense(1, activation = “sigmoid”)
model <— keras_model(inputs, outputs)
➊ 该模型期望的是尺寸为 180 × 180 的 RGB 图像。
➋ 通过将它们除以 255 来将输入重新缩放到 [0, 1] 范围内。
让我们看看随着每一层的连续变化,特征图的维度如何改变:
model
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0235-01.jpg
对于编译步骤,我们将继续使用 RMSprop 优化器,因为通常情况下我们会以单个 sigmoid 单元结束模型,所以我们将使用二元交叉熵作为损失函数(作为提醒,在第六章的表 6.1 中可以查看在各种情况下使用哪个损失函数的速查表)。
列表 8.8 配置模型进行训练
model %>% 编译(损失 = “binary_crossentropy”,
优化器 = “rmsprop”
指标 = “准确率”)
8.2.4 数据预处理
正如你现在所知道的,数据在进入模型之前应该被格式化为适当预处理的浮点张量。目前,数据作为 JPEG 文件存在于驱动器上,因此将其输入模型的步骤大致如下:
-
1 读取图片文件。
-
2 将 JPEG 内容解码为 RGB 像素网格。
-
3 将这些转换为浮点张量。
-
4 调整它们为共享大小(我们将使用 180 × 180)。
-
5 将它们打包成批次(我们将使用 32 张图像的批次)。
这可能看起来有点令人生畏,但幸运的是,Keras 提供了自动处理这些步骤的实用工具。特别是,Keras 具有实用函数 image_dataset_ from_directory(),它让你快速设置一个数据管道,可以自动将磁盘上的图像文件转换为预处理张量的批次。这是我们将在这里使用的方法。
调用 image_dataset_from_directory(directory) 首先会列出目录的子目录,并假定每个子目录都包含一个类别的图像。然后,它将索引每个子目录中的图像文件。最后,它将创建并返回一个 TF 数据集对象,配置为读取这些文件、对它们进行洗牌、将它们解码为张量、将它们调整为共享大小并将它们打包成批次。
列表 8.9 使用 image_dataset_from_directory 读取图像
训练数据集 <—
从目录创建图像数据集(new_base_dir / “train”)
图像大小 = c(180, 180)
批量大小 = 32)
验证数据集 <—
从目录创建图像数据集(new_base_dir / “validation”)
图像大小 = c(180, 180)
批量大小 = 32)
测试数据集 <—
从目录创建图像数据集(new_base_dir / “test”)
图像大小 = c(180, 180)
批量大小 = 32)
理解 tfdatasets
tfdatasets 包可用于为机器学习模型创建高效的输入管道。其核心对象类型是 TF 数据集。
TF 数据集对象是可迭代的:你可以在它上面调用 as_iterator() 来生成一个迭代器,然后在迭代器上重复调用 iter_next() 来生成数据序列。通常,你会使用 TF 数据集对象来生成输入数据和标签的批次。你可以直接将 TF 数据集对象传递给 Keras 模型的 fit() 方法。
TF 数据集对象处理了许多关键特性,否则你自己实现起来可能会很麻烦,特别是异步数据预取(在模型处理上一个批次数据的同时预处理下一个批次数据,这样可以保持执行流畅,没有中断)。
tfdatasets 包提供了一个函数式 API,用于修改数据集。这里是一个快速示例:让我们从一个整数序列的 R 数组创建一个 TF Dataset 实例。我们将考虑 100 个样本,其中每个样本是一个大小为 6 的向量(换句话说,我们的起始 R 数组是一个形状为 (100, 6) 的矩阵):
library(tfdatasets)
example_array <— array(seq(100*6), c(100, 6))
head(example_array)
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0237-01.jpg
dataset <— tensor_slices_dataset(example_array)➊
➊ tensor_slices_dataset() 函数可以用来从一个 R 数组或一个(可选命名的)R 数组列表创建 TF Dataset。
起初,我们的数据集只产生单个样本:
dataset_iterator <— as_iterator(dataset)
for(i in 1:3) {
element <— iter_next(dataset_iterator)
打印(element)
}
tf.Tensor([ 1 101 201 301 401 501], shape=(6), dtype=int32)
tf.Tensor([ 2 102 202 302 402 502], shape=(6), dtype=int32)
tf.Tensor([ 3 103 203 303 403 503], shape=(6), dtype=int32)
注意,默认情况下 TF Dataset 迭代器会产生 Tensorflow 张量。这通常是你想要的,也是 fit() 方法的最合适类型。然而,在某些情况下,你可能更喜欢迭代器产生 R 数组的批次;在这种情况下,你可以调用 as_array_iterator() 而不是 as_iterator():
dataset_array_iterator <— as_array_iterator(dataset)
for(i in 1:3) {
element <— iter_next(dataset_array_iterator)
str(element)
}
int [1:6(1d)] 1 101 201 301 401 501
int [1:6(1d)] 2 102 202 302 402 502
int [1:6(1d)] 3 103 203 303 403 503
我们可以使用 dataset_batch() 来对数据进行分批处理:
batched_dataset <— dataset %>%
dataset_batch(3)
batched_dataset_iterator <— as_iterator(batched_dataset)
for(i in 1:3) {
element <— iter_next(batched_dataset_iterator)
打印(element)
}
tf.Tensor(
[ [ 1 101 201 301 401 501]
[ 2 102 202 302 402 502]
[ 3 103 203 303 403 503]], shape=(3, 6), dtype=int32)
tf.Tensor(
[ [ 4 104 204 304 404 504]
[ 5 105 205 305 405 505]
[ 6 106 206 306 406 506]], shape=(3, 6), dtype=int32)
tf.Tensor(
[ [ 7 107 207 307 407 507]
[ 8 108 208 308 408 508]
[ 9 109 209 309 409 509]], shape=(3, 6), dtype=int32)
更广泛地说,我们可以访问一系列有用的数据集方法,比如
-
dataset_shuffle(buffer_size)—在缓冲区内对元素进行洗牌
-
dataset_prefetch(buffer_size)—预取 GPU 内存中的元素缓冲,以实现更好的设备利用率
-
dataset_map(fn)—对数据集的每个元素应用任意转换(函数 fn,它期望接受数据集产生的单个元素作为输入)
dataset_map() 方法特别常用。这里是一个例子。我们将使用它来将我们的玩具数据集中的元素重新塑形,从形状 (6) 到形状 (2, 3):
reshaped_dataset <— dataset %>%
dataset_map(function(element) tf$reshape(element, shape(2, 3)))➊
reshaped_dataset_iterator <— as_iterator(reshaped_dataset)
for(i in 1:3) {
element <— iter_next(reshaped_dataset_iterator)
打印(element)
}
tf.Tensor(
[[ 1 101 201]
[301 401 501]], 形状=(2, 3), 数据类型=int32)
tf.Tensor(
[[ 2 102 202]
[302 402 502]], 形状=(2, 3), 数据类型=int32)
tf.Tensor(
[[ 3 103 203]
[303 403 503]],
形状=(2, 3), 数据类型=int32)
➊ 请注意,tf$reshape() 使用 C 风格(行主序)语义进行重塑。
在本章中,您将看到更多 dataset_map() 操作。
让我们看看这些 Dataset 对象之一的输出:它产生 180 × 180 的 RGB 图像批次(形状为 (32, 180, 180, 3))和整数标签(形状为 (32))。每个批次中有 32 个样本(批量大小)。
列表 8.10 显示由 Dataset 产生的数据和标签的形状
c (data_batch, labels_batch) %<—% iter_next(as_iterator(train_dataset)) data_batch$形状
TensorShape([32, 180, 180, 3])
标签批次$形状
TensorShape([32])
让我们在我们的数据集上拟合模型。我们将使用 fit() 中的 validation_data 参数在单独的 TF Dataset 对象上监视验证指标。
请注意,我们还将使用 callback_model_checkpoint() 在每个时期之后保存模型。我们将配置它,指定保存文件的路径,以及参数 save_best_only = TRUE 和 monitor = “val_loss”:它们告诉回调函数仅在当前 val_loss 度量值低于训练期间任何以前时间的度量值时才保存新文件(覆盖任何先前的文件)。这确保了您保存的文件始终包含模型的状态,对应于其在验证数据上的性能最佳的训练时期。因此,如果我们开始过度拟合,我们不必重新训练一个新模型以进行更少数量的时期:我们可以重新加载我们保存的文件。
列表 8.11 使用 TensorFlow Dataset 拟合模型
回调函数 <— 列表(
callback_model_checkpoint(
文件路径 = “convnet_from_scratch.keras”,
save_best_only = TRUE,
monitor = “val_loss”
)
)
历史 <— 模型 %>%
fit(
train_dataset,
时期 = 30,
验证数据 = 验证 _dataset,
回调函数 = 回调函数
)
让我们绘制模型在训练和验证数据上的损失和准确性,以便在训练过程中进行对比(见 图 8.9)。
列表 8.12 显示训练期间的损失和准确性曲线
绘制(历史)
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0240-01.jpg
图 8.9 简单卷积网络的训练和验证指标。
这些图表特征是过度拟合的特征。训练准确度随时间线性增加,直到接近 100%,而验证准确度在 75% 处达到峰值。验证损失在仅 10 个时期后达到最小值,然后增加,而训练损失在训练过程中保持线性减少。
让我们检查测试准确度。我们将重新加载模型,以评估它在开始过度拟合之前的状态。
列表 8.13 在测试集上评估模型
测试模型 <— load_model_tf(“convnet_from_scratch.keras”)
result <— evaluate(test_model, test_dataset)
cat(sprintf(“测试准确度:%.3f\n”, result[“accuracy”]))
测试准确度:0.740
我们得到了 74% 的测试准确度。(由于神经网络初始化的随机性,你可能会得到略有不同的数字。)
由于我们的训练样本相对较少(2,000),过拟合将是我们关注的首要问题。你已经了解到一些可以帮助缓解过拟合的技术,例如随机失活和权重衰减(L2 正则化)。现在我们要使用一种新的技术,针对计算机视觉的,几乎在使用深度学习模型处理图像时通用的一种技术:数据增强。
8.2.5 使用数据增强
过拟合是由于样本数量太少而导致的,使得您无法训练出能够泛化到新数据的模型。如果有无限的数据,您的模型将接触到手头数据分布的每一个可能的方面:您永远不会过拟合。数据增强采取的方法是通过对现有训练样本进行一系列随机变换来生成更多的训练数据,从而增强样本。目标是,在训练时,您的模型永远不会看到完全相同的图片两次。这有助于使模型接触到数据的更多方面,从而更好地泛化。
在 Keras 中,可以通过在模型开头添加一些 数据增强层 来完成这个任务。让我们从一个示例开始:以下的 keras_model_sequential() 链接了几个随机图像转换。在我们的模型中,我们会在 layer_rescaling() 之前包含它。
列表 8.14 定义要添加到图像模型中的数据增强阶段
data_augmentation <— keras_model_sequential() %>%
layer_random_flip(“horizontal”) %>%
layer_random_rotation(0.1) %>%
layer_random_zoom(0.2)
这些只是可用的几个层之一(更多请参阅 Keras 文档)。让我们快速浏览一下这段代码:
-
layer_random_flip(“horizontal”)—将通过它的随机 50% 的图像进行水平翻转
-
layer_random_rotation(0.1)—将输入图像随机旋转一个范围为 [-10%,+10%] 的值(这些是完整圆的一部分——以角度表示,范围将是 [-36 度,+36 度])
-
layer_random_zoom(0.2)—将图像放大或缩小一个范围在 [-20%,+20%] 内的随机因子
让我们来看看增强后的图片(参见 图 8.10)。
列表 8.15 显示一些随机增强的训练图像
library(tfdatasets)
batch <— train_dataset %>%
as_iterator() %>%
iter_next()
c(images, labels) %<—% batch
par(mfrow = c(3, 3), mar = rep(.5, 4))➊
image <— images[1, , , ]
plot(as.raster(as.array(image), max = 255))➋
for (i in 2:9) {
augmented_images <— data_augmentation(images)➌
augmented_image <— augmented_images[1, , , ]
plot(as.raster(as.array(augmented_image), max = 255)➍
}
➊ 准备用于九张图片的图形设备。
➋ 绘制批次的第一张图片,不进行增强。
➌ 将增强阶段应用于图像批次。
➍ 显示输出批次中的第一张图像。对于每个八次迭代,这是同一图像的不同增强。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0242-01.jpg
图 8.10 通过随机数据增强生成非常好的狗的变化
如果我们使用这个数据增强配置来训练一个新模型,那么模型将永远不会看到相同的输入两次。但是它看到的输入仍然存在很高的相关性,因为它们来自少量原始图像——我们无法产生新的信息;我们只能重新组合现有的信息。因此,这可能不足以完全消除过拟合。为了进一步对抗过拟合,我们还会在密集连接分类器之前向我们的模型添加一个dropout()层。
关于随机图像增强层,你应该知道的最后一件事:就像layer_dropout()一样,在推理期间(当我们调用predict()或evaluate()时),它们是不活跃的。在评估期间,我们的模型的行为与不包括数据增强和 dropout 时完全相同。
列表 8.16 定义一个包括图像增强和 dropout 的新卷积神经网络
inputs <— layer_input(shape = c(180, 180, 3))
outputs <— inputs %>%
data_augmentation() %>%
layer_rescaling(1 / 255) %>%
layer_conv_2d(filters = 32, kernel_size = 3, activation = “relu”) %>%
layer_max_pooling_2d(pool_size = 2) %>% layer_conv_2d(filters = 64, kernel_size = 3, activation = “relu”) %>%
layer_max_pooling_2d(pool_size = 2) %>%
layer_conv_2d(filters = 128, kernel_size = 3, activation = “relu”) %>%
layer_max_pooling_2d(pool_size = 2) %>%
layer_conv_2d(filters = 256, kernel_size = 3, activation = “relu”) %>%
layer_max_pooling_2d(pool_size = 2) %>%
layer_conv_2d(filters = 256, kernel_size = 3, activation = “relu”) %>%
layer_flatten() %>%
layer_dropout(0.5) %>%
layer_dense(1, activation = “sigmoid”)
model <— keras_model(inputs, outputs)
model %>% compile(loss = “binary_crossentropy”,
optimizer = “rmsprop”,
metrics = “accuracy”)
让我们使用数据增强和 dropout 训练模型。因为我们预计过拟合将在训练期间晚得多,所以我们将训练三倍的轮次——一百轮。
列表 8.17 训练正则化的卷积神经网络
callbacks <— list(
callback_model_checkpoint(
filepath = “convnet_from_scratch_with_augmentation.keras”,
save_best_only = TRUE,
monitor = “val_loss”
)
)
history <— model %>% fit(
train_dataset,
epochs = 100,
validation_data = validation_dataset,
callbacks = callbacks
)
让我们再次绘制结果:参见图 8.11。由于数据增强和 dropout,我们开始在很晚的时候过拟合,大约在 60–70 轮之后(对比原始模型的 10 轮)。验证准确率最终保持在 80–85% 的范围内,这是我们第一次尝试的显著改进:
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0244-01.jpg
图 8.11 使用数据增强的训练和验证指标
让我们检查测试准确率。
列表 8.18 在测试集上评估模型
test_model <— load_model_tf(“convnet_from_scratch_with_augmentation.keras”)
结果 <— evaluate(test_model, test_dataset)
cat(sprintf(“测试准确率:%.3f\n”, result[“accuracy”]))
测试准确率:0.814
我们得到了 81.4%的测试准确率。看起来不错!如果您正在运行代码,请确保保留保存的文件(convnet_from_scratch_with_augmentation.keras),因为我们将在下一章中用到它进行一些实验。
通过进一步调整模型的配置(例如每个卷积层的滤波器数量,或者模型中的层数),我们可能会获得更高的准确率,可能高达 90%。但是仅通过从头训练我们自己的卷积网络,要达到更高的准确率将会很困难,因为我们的数据量太少。为了提高这个问题上的准确率,我们的下一步将是使用预训练模型,这是接下来两节的重点。
8.3 利用预训练模型
在小图像数据集上进行深度学习的一种常见且非常有效的方法是使用预训练模型。预训练模型是以前在大型数据集上训练过的模型,通常是在大规模图像分类任务上。如果这个原始数据集足够大且足够通用,那么预训练模型学到的特征的空间层次结构可以有效地充当视觉世界的通用模型,因此,它的特征可以对许多不同的计算机视觉问题提供有用的信息,即使这些新问题可能涉及与原始任务完全不同的类别。例如,您可以在 ImageNet 上训练模型(其中大多数类别是动物和日常物品),然后将这个训练好的模型重新用于识别图像中的家具项目等完全不同的目标。深度学习与许多较老的、浅层的学习方法相比的一个关键优势是,它学到的特征在不同问题之间的可移植性,这使得深度学习对于小数据问题非常有效。
在这种情况下,让我们考虑一个在 ImageNet 数据集上训练的大型卷积网络(140 万张带标签的图像和 1000 个不同的类)。ImageNet 包含许多动物类别,包括不同种类的猫和狗,因此您可以期望它在猫狗分类问题上表现良好。
我们将使用由 Karen Simonyan 和 Andrew Zisserman 于 2014 年开发的 VGG16 架构¹。尽管这是一个较老的模型,远非当前技术水平的最新状态,并且比许多其他近期模型更加笨重,但我选择它是因为它的架构与你已经熟悉的相似,并且不需要引入任何新概念就能轻松理解。这可能是你第一次遇到这些可爱的模型名称之一——VGG、ResNet、Inception、Xception 等等;如果你继续进行计算机视觉的深度学习,你将经常遇到它们。
有两种使用预训练模型的方式:特征提取和微调。我们将会涵盖这两种方法。让我们从特征提取开始。
8.3.1 使用预训练模型进行特征提取
特征提取包括使用先前训练好的模型学到的表示从新样本中提取有趣的特征。然后这些特征通过一个从头开始训练的新分类器。
正如你之前所看到的,用于图像分类的卷积神经网络包括两部分:它们首先是一系列的池化和卷积层,然后是一个全连接的分类器。第一部分被称为模型的卷积基。在卷积神经网络中,特征提取是指利用先前训练好的网络的卷积基对新数据进行传递,并在其之上训练一个新的分类器(参见图 8.12)。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0246-01.jpg
图 8.12 保持相同的卷积基进行分类器交换
为什么仅重用卷积基?我们能否也重用全连接的分类器?一般来说,应该避免这样做。原因是卷积基学到的表示可能更通用,因此更易重用:卷积神经网络的特征图是图片中通用概念的存在图,无论当前的计算机视觉问题是什么,该特征图都可能是有用的。但是分类器学到的表示将必然特定于模型训练的类别集,它们只包含关于这个或那个类别在整个图片中出现概率的信息。此外,在全连接层中发现的表示不再包含有关物体在输入图像中位置的任何信息;这些层丢弃了空间概念,而卷积特征图仍然描述着物体的位置。对于物体位置很重要的问题,全连接特征基本上是无用的。
请注意,特定卷积层提取的表示的一般性(因此也是可重用性)取决于模型中的层的深度。在模型中更早的层提取局部的高度通用的特征图(例如视觉边缘,颜色和纹理),而更高的层提取更抽象的概念(如“猫耳”或“狗眼”)。因此,如果你的新数据集与原始模型训练的数据集差异很大,你可能最好只使用模型的前几层进行特征提取,而不是整个卷积基。
在这种情况下,由于 ImageNet 类集包含多个狗和猫类别,重用原始模型的密集连接层中包含的信息可能是有益的。但我们选择不这样做,以涵盖新问题的类别集不重叠于原始模型的类别集的更普遍情况。让我们通过使用在 ImageNet 上训练的 VGG16 网络的卷积基础,从猫和狗图像中提取有趣的特征,然后在这些特征的顶部训练一个猫狗分类器来将其付诸实践。
VGG16 模型等等,都预先在 Keras 中打包。它们都作为以 application_ 前缀开头的函数导出。许多其他图像分类模型(都是在 ImageNet 数据集上预训练的)都作为 Keras 应用程序的一部分提供:
-
Xception
-
Mobilenet
-
DenseNet
-
ResNet
-
EfficientNet
-
等等
让我们实例化 VGG16 模型。
列表 8.19 实例化 VGG16 卷积基础
conv_base <— application_vgg16(
weights = “imagenet”,
include_top = FALSE,
input_shape = c(180, 180, 3)
)
我们向应用函数传递了三个参数:
-
weights 指定初始化模型的权重检查点。
-
include_top 指的是是否在网络顶部包含(或不包含)密集连接分类器。默认情况下,这个密集连接分类器对应于 ImageNet 的 1,000 个类别。因为我们打算使用自己的密集连接分类器(只有两个类别:猫和狗),所以我们不需要包含它。
-
input_shape 是我们将要馈送到网络中的图像张量的形状。这个参数是可选的:如果我们不传递它,网络将能够处理任何大小的输入。在这里,我们传递它,以便我们可以可视化(在以下摘要中)随着每个新的卷积和池化层的形状如何收缩。
这是 VGG16 卷积基础架构的详细信息。它与你已经熟悉的简单卷积网络类似:
conv_base
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0248-01.jpg
最终的特征图形状为 (5, 5, 512)。这就是我们将要在其顶部添加一个密集连接分类器的特征图。
此时,我们可以采取两种方式:
-
在我们的数据集上运行卷积基础,将其输出(数组)记录到磁盘上的文件数组中,然后使用这些数据作为章节 4 中所见的独立、密集连接分类器的输入。这种解决方案运行速度快,成本低,因为它只需要对每个输入图像运行一次卷积基础,而卷积基础是流水线中成本最高的部分。但出于同样的原因,这种技术不允许我们使用数据增强。
-
扩展我们已有的模型(conv_base),在其顶部添加密集层,并在输入数据上端到端地运行整个模型。这将允许我们使用数据增强,因为每个输入图像在被模型看到时都会经过卷积基。但出于同样的原因,这种技术比第一个技术要昂贵得多。
我们将涵盖这两种技术。让我们逐步介绍设置第一种技术所需的代码:记录 conv_base 在我们的数据上的输出,并将这些输出用作新模型的输入。
无数据增强的快速特征提取
我们将首先通过调用 conv_base 模型的 predict() 方法在我们的训练、验证和测试数据集上提取特征作为 R 数组。
让我们遍历我们的数据集以提取 VGG16 特征。
清单 8.20 提取 VGG16 特征和相应的标签
get_features_and_labels <- function(dataset) {
n_batches <- length(dataset)
all_features <- vector(“list”, n_batches)
all_labels <- vector(“list”, n_batches)
iterator <- as_array_iterator(dataset)
for (i in 1:n_batches) {
c(images, labels) %<-% iter_next(iterator)
preprocessed_images <- imagenet_preprocess_input(images)
features <- conv_base %>% predict(preprocessed_images)
all_labels[[i]] <- labels
all_features[[i]] <- features
}
all_features <- listarrays::bind_on_rows(all_features)
all_labels <- listarrays::bind_on_rows(all_labels)➊
list(all_features, all_labels)
}
c(train_features, train_labels) %<-% get_features_and_labels(train_dataset)
c(val_features, val_labels) %<-% get_features_and_labels(validation_dataset)
c(test_features, test_labels) %<-% get_features_and_labels(test_dataset)
➊将一系列 R 数组沿着第一个轴(批处理维度)组合在一起。
重要的是,predict() 仅期望图像,而不是标签,但我们当前的数据集生成的批次包含图像及其标签。此外,VGG16 模型期望经过函数 imagenet_preprocess_input() 预处理的输入,该函数将像素值缩放到合适的范围。提取的特征目前的形状为(样本数,5,5,512):
dim(train_features)
[1] 2000 5 5 512
在这一点上,我们可以定义我们的密集连接的分类器(请注意使用了 dropout 进行正则化),并在我们刚刚记录的数据和标签上对其进行训练。
清单 8.21 定义和训练密集连接的分类器
inputs <- layer_input(shape = c(5, 5, 512))
outputs <- inputs %>%
layer_flatten() %>%➊
layer_dense(256) %>%
layer_dropout(.5) %>%
layer_dense(1, activation = “sigmoid”)
model <- keras_model(inputs, outputs)
model %>% compile(loss = “binary_crossentropy”,
optimizer = “rmsprop”,
metrics = “accuracy”)
callbacks <- list(
callback_model_checkpoint(
filepath = “feature_extraction.keras”,
save_best_only = TRUE,
monitor = “val_loss”
)
)
history <- model %>% fit(
train_features, train_labels,
epochs = 20,
validation_data = list(val_features, val_labels),
callbacks = callbacks
)
➊注意在传递特征给 layer_dense() 之前使用了 layer_flatten()。
训练非常快,因为我们只需处理两个密集层 - 一个时代少于一秒,甚至在 CPU 上也是如此。
让我们在训练过程中查看损失和准确率曲线(见 图 8.13)。
清单 8.22 绘制结果
绘制(history)
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0250-01.jpg
图 8.13 普通特征提取的训练和验证指标
我们达到了约 97% 的验证准确率 - 比我们在上一节使用从头开始训练的小模型取得的结果要好得多。然而,这有点不公平的比较,因为 ImageNet 包含许多狗和猫实例,这意味着我们的预训练模型已经具有了所需任务的确切知识。当您使用预训练特征时,情况并非总是如此。
然而,图表也显示,尽管使用了相当大的丢弃率,但我们几乎从一开始就出现了过拟合。这是因为这种技术不使用数据增强,而数据增强对于防止小图像数据集过拟合是至关重要的。
特征提取与数据增强
现在让我们回顾我提到的第二种做特征提取的技术,它要慢得多,更昂贵,但允许我们在训练过程中使用数据增强:创建一个将卷积基与新的密集分类器链在一起的模型,并在输入上端到端地训练它。
为此,我们首先会 冻结卷积基。冻结层或一组层意味着在训练期间阻止它们的权重更新。如果我们不这样做,那么先前由卷积基学到的表示将在训练期间被修改。因为顶部的密集层是随机初始化的,所以会通过网络传播非常大的权重更新,有效地破坏先前学到的表示。在 Keras 中,我们通过调用 freeze_weights() 来冻结层或模型。
清单 8.23 实例化和冻结 VGG16 卷积基
conv_base <- application_vgg16(
权重 = “imagenet”,
include_top = FALSE)
freeze_weights(conv_base)
调用 freeze_weights() 会清空层或模型的可训练权重列表。
清单 8.24 打印在冻结之前和之后的可训练权重列表
unfreeze_weights(conv_base)
cat(“这是可训练权重的数量”,
“冻结卷积基之前:”
length(conv_base$trainable_weights), “\n”)
这是在冻结卷积基之前的可训练权重数量:26
freeze_weights(conv_base)
cat(“这是可训练权重的数量”,
“冻结卷积基之后:”
length(conv_base$trainable_weights), “\n”)
冻结卷积基之后的可训练权重数量为:0
现在我们可以创建一个新模型,将特征链接在一起
-
1 数据增强阶段
-
2 我们冻结的卷积基
-
3 一个密集分类器
数据增强 <- keras_model_sequential() %>%
layer_random_flip(“horizontal”) %>%
layer_random_rotation(0.1) %>%
layer_random_zoom(0.2)
输入 <- layer_input(shape = c(180, 180, 3))
输出 <- 输入 %>%
data_augmentation() %>%➊
imagenet_preprocess_input() %>%➋
conv_base() %>%
layer_flatten() %>%
layer_dense(256) %>%
layer_dropout(0.5) %>%
layer_dense(1, activation = “sigmoid”)
model <- keras_model(inputs, outputs)
model %>% compile(loss = “binary_crossentropy”,
优化器 = “rmsprop”,
指标 = “准确度”)
➊应用数据增强。
➋ 应用输入值缩放。
使用此设置,只有我们添加的两个密集层的权重将会被训练。总共有四个权重张量:每层两个(主要权重矩阵和偏置向量)。注意,为了使这些更改生效,您必须先编译模型。如果您在编译之后修改了权重的可训练性,那么您应该重新编译模型,否则这些更改将被忽略。
让我们训练我们的模型。由于数据增强,模型要过拟合的时间要长得多,所以我们可以训练更多的 epochs——让我们做 50 个。
这项技术够昂贵,只有在有 GPU 的情况下才尝试,CPU 上无法操作。如果无法在 GPU 上运行代码,那么前一项技术就是最佳选择:
回调 <- list(
回调模型检查点(
文件路径 = “使用数据增强进行特征提取.keras”,
仅保存最佳结果 = TRUE,
监控 = “val_loss”
)
)
history <- model %>% fit(
训练数据集,
epochs = 50,
验证数据 = 验证数据集,
回调 = 回调
)
让我们再次绘制结果(参见图 8.14)。如您所见,我们达到了超过 98%的验证准确度。这是相对于先前模型的显著提高。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0253-01.jpg
图 8.14 使用数据增强进行特征提取的训练和验证指标
让我们检查一下测试准确度。
清单 8.25 在测试集上评估模型
测试模型 <- load_model_tf(
“使用数据增强进行特征提取.keras”)
result <- evaluate(test_model, test_dataset)
cat(sprintf(“测试精度:%.3f\n”, result[“准确度”]))
测试精度:0.977
我们得到了 97.7%的测试准确度。与以前的测试准确度相比,这只是一次适度的提高,这有点令人失望,考虑到在验证数据上的强劲结果。模型的准确度总是取决于您对其进行评估的样本集。有些样本集可能比其他样本集更难,对一个集合的强烈结果未必会完全转化到其他所有集合。
8.3.2 微调预训练模型
进一步复用模型的一个广泛使用的技巧是微调(fine-tuning),与特征提取互补(见图 8.15)。 微调包含解冻一个用于特征提取的模型基底的顶部几层,同时联合训练这个新添加的部分(在本例中是全连接分类器)和这些顶部层。这称为微调,是因为它会稍微调整正在复用的模型的更抽象表示,使其更相关于手头的问题。
我之前说过,必须冻结 VGG16 的卷积基,才能够在其上训练随机初始化的分类器。出于同样的原因,只有在距离分类器先前已经训练好的情况下,才能微调卷积基的顶部层。如果分类器还没有进行训练,训练过程中传播的错误信号会过大,并且微调的层之前学习到的表示将被破坏。因此微调网络的步骤如下:
-
1 在已经训练好的基础网络上添加自定义网络。
-
2 冻结基础网络。
-
3 训练我们添加的部分。
-
4 解冻基础网络中的一些层。(请注意,不应解冻“批量标准化”层,因为在 VGG16 中没有这样的层。批量标准化及其对微调的影响将在下一章中解释。)
-
5 联合训练这两个层和我们添加的部分。
进行特征提取时,前三个步骤已经完成,现在进行第四个步骤:解冻 conv_base,然后冻结其中的各个层。
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0254-01.jpg
图 8.15 调整 VGG16 网络的最后一个卷积块
提醒一下,这就是我们的卷积基长这样子:
conv_base
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0255-01.jpg
我们将调整最后三个卷积层,这意味着所有层,包括 block4_pool 之前的层都应该保持冻结状态,而 block5_conv1、block5_conv2 和 block5_conv3 这三个层则应该可训练。
为什么不能调整更多层?为什么不能调整整个卷积基?其实可以,但要考虑以下几点:
-
卷积基中越往前的层编码的是更加通用、可重复使用的特征,而越往后的层编码的是更加专业化的特征。 fine-tune 更加专业化的特征更有用,因为这些是需要在新问题上转化的。对低层进行调整,收益会逐渐变小。
-
如果要训练的参数越多,过度拟合的风险就越大。卷积基有 1500 万个参数,因此在小数据集上尝试训练它是很冒险的。
因此,在这种情况下,只微调卷积基准模型的前两层或三层是一个不错的策略。让我们从前一个例子结束的地方开始设定这个。
列表 8.26 冻结直到倒数第四层的所有层
unfreeze_weights(conv_base, from = -4) conv_base➊
https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/dl-r-2e/img/f0256-01.jpg
➊from = -4 是 length(conv_base$layers) + 1 - 4 的简写
现在,我们可以开始微调模型了。我们将使用 RMSprop 优化器,并使用非常低的学习率。之所以使用低学习率,是因为我们希望限制对三层表示所做修改的幅度。太大的更新可能会损害这些表示。
列表 8.27 微调模型
model %>% compile(
loss = “binary_crossentropy”,
optimizer = optimizer_rmsprop(learning_rate = 1e-5),
metrics = “accuracy”
)
callbacks <- list(
callback_model_checkpoint(
filepath = “fine_tuning.keras”,
save_best_only = TRUE,
monitor = “val_loss”
)
)
history <- model %>% fit(
train_dataset,
epochs = 30,
validation_data = validation_dataset,
callbacks = callbacks
)
现在,我们可以对测试数据集评估这个模型:
model <- load_model_tf(“fine_tuning.keras”)
result <- evaluate(model, test_dataset)
cat(sprintf(“测试准确率:%.3f\n”, result[“accuracy”]))
测试准确率:0.985
在这里,我们获得了 98.5%的测试准确率(再次强调,你自己的结果可能和这个相差不超过一个百分点)。在原始的 Kaggle 竞赛中,这将是顶级结果之一。但是这并不是一个公平的比较,因为我们使用了预训练的特征,这些特征已经包含了关于猫和狗的先前知识,而竞争对手当时无法使用。
从正面来说,借助现代深度学习技术,我们仅使用了竞赛数据中可用的一小部分训练数据(约占总量的 10%)就达到了这一结果。在可以训练 2,000 个样本和 20,000 个样本之间,存在巨大的差距!
现在,你已经掌握了一套处理图像分类问题的工具,特别是处理小数据集时。
摘要
-
卷积网络是计算机视觉任务中最好的机器学习模型类型。即使在非常小的数据集上,也可以训练一个具有不错结果的模型。
-
卷积网络通过学习一系列模块化的模式和概念来表示视觉世界。
-
在小数据集上,过拟合是主要问题。数据增强是处理图像数据时对抗过拟合的有效手段。
-
通过特征提取,可以很容易地在新数据集上重用现有的卷积网络。这是处理小型图像数据集的一项有价值的技术。
-
作为特征提取的补充,你可以使用微调(fine-tuning),它可以根据现有模型先前学到的某些表示来适应一个新的问题。这会进一步提高性能。
- ¹ Karen Simonyan 和 Andrew Zisserman,《非常深的卷积网络用于大规模图像识别》,arXiv(2014),
arxiv.org/abs/1409.1556。
更多推荐


所有评论(0)