深度学习快速参考(一)
零、前言
《深度学习快速参考》演示了使用深度学习的快速实用方法。 它着重于现实生活中的问题,并且仅提供了足够的理论和数学知识来加深读者对该主题的理解。 深度学习是机器学习中令人兴奋的快速节奏分支,但它也是一个可以涉足的领域。 在这个领域,每天都会进行大量的详细而复杂的研究,而这可能会令人不知所措。 在本书中,我着重向您传授将深度学习应用于各种实际问题的技能。 我对这本书的最大希望是,它将为您提供使用深度学习技术解决机器学习问题所需的工具。
这本书是给谁的
我是一名实践中的数据科学家,我在写这本书时牢记其他实践中的数据科学家和机器学习工程师。 如果您是应用深度学习的软件工程师,那么这本书也很适合您。
如果您是一名深度学习研究人员,那么这本书并不适合您。 但是,您仍然应该拿起副本,以便批评这本书缺乏证明和数学上的严格性。
如果您是一名学者或教育家,那么这本书绝对适合您。 在过去的 3 年中,我在伊利诺伊大学斯普林菲尔德分校教授了数据科学的调查数据(去草原之星!),这样做,我有机会启发了许多未来的机器学习人员。 这种经历启发了我创作这本书。 我认为这样的书是帮助学生提高对一个非常复杂的主题的兴趣的好方法。
本书涵盖的内容
第 1 章“深度学习的基础知识”,回顾了有关神经网络操作的一些基础知识,涉及了优化算法,讨论了模型验证,并讨论了建立开发环境的内容。 适用于构建深度神经网络。
第 2 章“使用深度学习解决回归问题”,您可以构建非常简单的神经网络来解决回归问题,并研究更深更复杂的模型对这些问题的影响。
第 3 章“使用 TensorBoard 监视网络训练”让您立即开始使用 TensorBoard,这是监视和调试未来模型的绝佳应用。
第 4 章“使用深度学习解决二分类问题”帮助您使用深度学习解决二分类问题。
第 5 章“使用 Keras 解决多分类问题”,带您进行多分类并探讨它们之间的区别。 它还讨论了管理过拟合和最安全的选择。
第 6 章“超参数优化”显示了两种独立的模型调整方法,一种是众所周知的且经过实战测试的方法,而另一种是最新方法。
第 7 章“从头开始训练 CNN”教您如何使用卷积网络对图像进行分类。
第 8 章“使用预训练的 CNN 的迁移学习”描述了如何应用迁移学习来从图像分类器中获得惊人的表现,即使数据很少。
第 9 章“从头开始训练 RNN”,讨论 RNN 和 LSTMS,以及如何将其用于时间序列预测问题。
第 10 章“从头开始用词嵌入训练 LSTM”继续我们关于 LSTM 的讨论,这次讨论的是自然语言分类任务。
第 11 章“训练 Seq2Seq 模型”帮助我们使用序列对模型进行序列化以进行机器翻译。
第 12 章“使用深度强化学习”引入了深度强化学习,并构建了可以为自治智能体提供动力的深度 Q 网络。
第 13 章“生成对抗网络”解释了如何使用生成对抗网络生成令人信服的图像。
充分利用这本书
- 我假设您已经对更传统的数据科学和预测建模技术(例如线性/逻辑回归和随机森林)有丰富的经验。 如果这是您第一次学习机器学习,那么对您来说可能有点困难。
- 我还假定您至少具有使用 Python 进行编程的经验,或者至少具有其他编程语言(如 Java 或 C++)。
- 深度学习是计算密集型的,我们在这里构建的某些模型需要 NVIDIA GPU 在合理的时间内运行。 如果您没有快速的 GPU,则可能希望在 Amazon Web Services 或 Google Cloud Platform 上使用基于 GPU 的云实例。
使用约定
本书中使用了许多文本约定。
CodeInText:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 这是一个示例:“这正是ModelCheckpoint回调为我们所做的。”
代码块设置如下:
def binary_accuracy(y_true, y_pred):
return K.mean(K.equal(y_true, K.round(y_pred)), axis=-1)
当我们希望引起您对代码块特定部分的注意时,相关的行或项目以粗体显示:
def build_network(input_features=None):
inputs = Input(shape=(input_features,), name="input")
x = Dense(32, activation='relu', name="hidden1")(inputs)
x = Dense(32, activation='relu', name="hidden2")(x)
x = Dense(32, activation='relu', name="hidden3")(x)
x = Dense(32, activation='relu', name="hidden4")(x)
x = Dense(16, activation='relu', name="hidden5")(x)
prediction = Dense(1, activation='linear', name="final")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='mean_absolute_error')
return model
任何命令行输入或输出的编写方式如下:
model-weights.00-0.971304.hdf5
model-weights.02-0.977391.hdf5
model-weights.05-0.985217.hdf5
粗体:表示新术语,重要单词或您在屏幕上看到的单词。 例如,菜单或对话框中的单词会出现在这样的文本中。 这是一个示例:“从管理面板中选择系统信息。”
警告或重要提示如下所示。
提示和技巧如下所示。
一、深度学习的基础
欢迎使用《深度学习快速参考》! 在本书中,我将尝试使需要解决深度学习问题的数据科学家,机器学习工程师和软件工程师更容易使用,实用和使用深度学习技术。 如果您想训练自己的深度神经网络并且陷入困境,那么本指南很有可能会有所帮助。
本书动手了,旨在作为实用指南,可以帮助您快速解决问题。 它主要供需要使用深度学习解决问题的经验丰富的机器学习工程师和数据科学家使用。 除了本章(其中提供了一些我们将要开始使用的术语,框架和背景知识)之外,它并不意味着要按顺序阅读。 每章均包含一个实际示例,并附有代码,一些最佳实践和安全选择。 我们希望您能跳到所需的章节并开始使用。
本书不会深入研究深度学习和神经网络的理论。 有许多可以提供这种背景知识的精彩书籍,我强烈建议您至少阅读其中一本(也许是参考书目,也可以只是建议)。 我们希望提供足够的理论和数学直觉来帮助您入门。
我们将在本章介绍以下主题:
- 深度神经网络架构
- 深度学习的优化算法
- 深度学习框架
- 构建用于深度学习的数据集
深度神经网络架构
深度神经网络架构的结构可能会因网络的应用而有很大差异,但它们都有一些基本组件。 在本节中,我们将简要讨论这些组件。
在本书中,我将深度神经网络定义为一个具有多个隐藏层的网络。 除此之外,我们不会尝试将成员限制为深度学习俱乐部。 因此,我们的网络可能只有不到 100 个神经元,甚至可能有数百万个。 我们可能会使用特殊的神经元层,包括卷积和循环层,但尽管如此,我们仍将所有这些都称为神经元。
神经元
神经元是神经网络的原子单位。 有时这是受到生物学启发的。 但是,这是另一本书的主题。 神经元通常排列成层。 在本书中,如果我指的是特定的神经元,则将使用符号n[k]^l,其中l是神经元所在的层, k是神经元编号 。 由于我们将使用遵循第 0 个表示法的编程语言,因此我的表示法也将基于第 0 个表示法。
大多数神经元的核心是两个共同起作用的函数:线性函数和激活函数。 让我们从较高的角度看一下这两个组成部分。
神经元线性函数
神经元的第一部分是线性函数,其输出是输入的总和,每个输入乘以一个系数。 这个函数实际上或多或少是线性回归。 这些系数通常在神经网络中称为权重。 例如,给定某些神经元,其输入特征为x1,x2和x3,输出z,则此线性分量或神经元线性函数将简单地为:
在给定数据的情况下,θ[1], θ[2], ..., θ[n]是权重或系数,b是偏差项。
神经元激活函数
神经元的第二个函数是激活函数,其任务是在神经元之间引入非线性。 Sigmoid 激活是一种常用的激活,您可能会通过逻辑回归来熟悉它。 它将神经元的输出压缩到输出空间,其中z的非常大的值被驱动为 1,而z的非常小的值被驱动为 0。
sigmoid 函数如下所示:
事实证明,激活函数对于中间神经元非常重要。 没有它,可以证明一堆具有线性激活的神经元(实际上不是激活,或更正式地说是z = z的激活函数)实际上只是一个线性函数。
在这种情况下,单个线性函数是不理想的,因为在许多情况下,我们的网络可能未针对当前问题指定。 也就是说,由于输入特征和目标变量之间的非线性关系(我们正在预测),网络无法很好地对数据建模。
不能用线性函数建模的函数的典型示例是排他的OR函数,如下图所示:
其他常见的激活函数是tanh函数和 ReLu 或整流线性激活。
双曲正切或tanh函数如下所示:
对于中间层,tanh通常比 Sigmoid 更好。 您可能会看到,tanh的输出将在[-1, 1]之间,而 Sigmoid 曲线的输出将为[0, 1]。 这种额外的宽度可为消失或爆炸的梯度问题提供一定的弹性,我们将在后面详细介绍。 到目前为止,仅需知道消失的梯度问题就可以使网络在早期的层中收敛非常慢(如果有的话)。 因此,使用tanh的网络趋于比使用 Sigmoid 激活的网络收敛更快。 也就是说,它们仍然不如 ReLu 快。
ReLu,或直线激活,简单定义为:
这是一个安全的赌注,我们在本书中的大部分时间都会使用它。 ReLu 不仅易于计算和微分,而且还可以抵抗消失的梯度问题。 ReLu 的唯一缺点是它的一阶导数未精确定义为 0。包括泄漏的 ReLu 在内的变体在计算上更加困难,但针对此问题更健壮。
为了完整起见,以下是 ReLu 的一些明显图表:
深度学习中的损失和成本函数
每个机器学习模型实际上都是从成本函数开始的。 简单来说,成本函数可让您衡量模型对训练数据的拟合程度。 在本书中,我们将损失函数定义为训练集中单个观测值的拟合正确性。 这样,成本函数通常将是整个训练集中损失的平均值。 稍后,当我们介绍每种类型的神经网络时,我们将重新讨论损失函数。 但是,请快速考虑线性回归的成本函数作为示例:
在这种情况下,损失函数为(y_hat - y)^2,这实际上是平方误差。 因此,我们的cost函数J实际上只是均方误差,或整个数据集的均方误差的平均值。 按照惯例,添加了项 1/2 以使某些微积分更干净。
正向传播过程
正向传播是我们尝试使用单个观测值中存在的特征预测目标变量的过程。 想象一下,我们有一个两层神经网络。 在正向传播过程中,我们将从观察中出现的特征x[1], x[2], ..., x[n]开始,然后将这些特征乘以它们在第 1 层中的关联系数,并为每个神经元添加一个偏差项。 之后,我们会将输出发送到神经元的激活。 之后,输出将被发送到下一层,依此类推,直到到达网络的末端,然后剩下网络的预测:
反向传播过程
一旦正向传播完成,我们就可以对每个数据点进行网络预测。 我们也知道数据点的实际值。 通常,将预测定义为y_hat,而将目标变量的实际值定义为y。
一旦y和y_hat都已知,就可以使用成本函数计算网络误差。 回想一下,代价函数是loss函数的平均值。
为了使学习在网络中发生,网络的误差信号必须从最后一层到最后一层通过网络层向后传播。 我们反向传播的目标是在网络中向后传播该误差信号,同时随着信号的传播使用误差信号来更新网络权重。 在数学上,要做到这一点,我们需要对权重进行微调,以使成本函数最小,从而最小化成本函数。 此过程称为梯度下降。
梯度是误差函数相对于网络内每个权重的偏导数。 可以使用链法则和上面各层的梯度逐层计算每个权重的梯度。
一旦知道了每一层的梯度,我们就可以使用梯度下降算法来最小化cost函数。
梯度下降将重复此更新,直到网络的误差最小化并且该过程收敛为止:
梯度下降算法将梯度乘以称为alpha的学习率,然后从每个权重的当前值中减去该值。 学习率是一个超参数。
随机和小批量梯度下降
上一节中描述的算法假定整个数据集都进行正向和相应的反向传递,因此将其称为批梯度下降。
进行梯度下降的另一种可能方法是一次使用一个数据点,并随着我们的更新网络权重。 此方法可能有助于加快网络可能停止收敛的鞍点附近的收敛速度。 当然,仅单个点的误差估计可能无法很好地近似于整个数据集的误差。
解决此问题的最佳解决方案是使用小型批量梯度下降,其中我们将采用称为小型批量的数据的随机子集来计算误差并更新网络权重。 这几乎总是最好的选择。 它还有一个额外的好处,即可以将非常大的数据集自然地拆分为多个块,这些块可以更容易地在计算机的内存中甚至跨计算机的内存中进行管理。
这是对神经网络最重要部分之一的极高层次的描述,我们认为这与本书的实际性质相符。 实际上,大多数现代框架都为我们处理了这些步骤。 但是,至少在理论上,它们无疑是值得了解的。 我们鼓励读者在时间允许的情况下更深入地进行向前和向后传播。
深度学习的优化算法
梯度下降算法不是唯一可用于优化网络权重的优化算法,但它是大多数其他算法的基础。 虽然了解每种优化算法都有可能获得博士学位,但我们将为一些最实用的内容专门介绍几句话。
梯度下降和动量
通过使用具有动量的梯度下降,可以通过增加方向学习的速度来加快梯度下降,从而使梯度在方向上保持恒定,而在方向缓慢学习时,梯度会在方向上波动。 它允许梯度下降的速度增加。
动量的工作原理是引入速度项,并在更新规则中使用该项的加权移动平均值,如下所示:
在动量的情况下,最通常将β设置为 0.9,通常这不是需要更改的超参数。
RMSProp 算法
RMSProp 是另一种算法,可以通过跨网络权重表示的多维空间,通过在某些方向上加快学习速度,并在其他方向上抑制振荡来加快梯度下降:
这具有在v[t]大的方向上进一步减少振荡的效果。
Adam 优化器
Adam 是已知表现最好的优化器之一,这是我的首选。 它可以很好地解决各种问题。 它将动量和 RMSProp 的最佳部分组合到一个更新规则中:
其中ε很小,可以防止被 0 除。
亚当通常是一个不错的选择,当您进行原型设计时,这是一个很好的起点,因此,从亚当开始可以节省一些时间。
深度学习框架
虽然仅使用 Python 的numpy从头开始构建和训练深度神经网络是绝对可能的,但这将花费大量的时间和代码。 在几乎每种情况下,使用深度学习框架都更加实用。
在本书中,我们将使用 TensorFlow 和 Keras 来使开发深度神经网络变得更加轻松和快捷。
什么是 TensorFlow?
TensorFlow 是一个可用于快速构建深度神经网络的库。 在 TensorFlow 中,我们到目前为止已涵盖的数学运算被表示为节点。 这些节点之间的边缘是张量或多维数据数组。 给定定义为图和损失函数的神经网络,TensorFlow 可以自动计算网络的梯度并优化图以最小化损失函数。
TensorFlow 是 Google 在 2015 年发布的一个开源项目。此后,它已经获得了很大的关注,并拥有庞大的用户社区。 虽然 TensorFlow 提供 Java,C++,Go 和 Python 的 API,但我们仅介绍 Python API。 本书使用了 Python API,因为它既是最常用的,也是开发新模型时最常用的 API。
通过在一个或多个图形处理单元上执行这些计算,TensorFlow 可以大大加快计算速度。 GPU 计算提供的加速已成为现代深度学习中的必要条件。
什么是 Keras?
尽管在 TensorFlow 中构建深度神经网络要比从头开始做起来容易得多,但 TensorFlow 仍然是一个非常底层的 API。 Keras 是一个高级 API,允许我们使用 TensorFlow(或 Theano 或 Microsoft 的 CNTK)快速构建深度学习网络。
用 Keras 和 TensorFlow 构建的模型是便携式的,也可以在本机 TensorFlow 中进行训练或使用。 TensorFlow 中构建的模型可以加载到 Keras 中并在其中使用。
TensorFlow 的流行替代品
那里还有许多其他很棒的深度学习框架。 我们之所以选择 Keras 和 TensorFlow,主要是因为其受欢迎程度,易用性,支持的可用性以及生产部署的准备就绪。 无疑还有其他有价值的选择。
我最喜欢的 TensorFlow 替代品包括:
- Apache MXNet:一个非常高表现的框架,带有一个名为 Gluon 的新命令式接口
- PyTorch:Facebook 最初开发的一种非常新颖且有希望的架构
- CNTK:也可以与 Keras 一起使用的 Microsoft 深度学习框架
尽管我确实坚信 Keras 和 TensorFlow 是本书的正确选择,但我也想承认这些出色的框架以及每个项目对领域做出的贡献。
TensorFlow 和 Keras 的 GPU 要求
在本书的其余部分,我们将使用 Keras 和 TensorFlow。 我们将探索的大多数示例都需要 GPU 来加速。 包括 TensorFlow 在内的大多数现代深度学习框架都使用 GPU 极大地加速了网络训练期间所需的大量计算。 如果没有 GPU,我们讨论的大多数模型的训练时间将过长。
如果您没有安装有 GPU 的计算机,则可以从包括 Amazon 的 Amazon Web Services 和 Google 的 Google Cloud Platform 在内的各种云提供商处租用基于 GPU 的计算实例。 对于本书中的示例,我们将在运行 Ubuntu Server 16.04 的 Amazon EC2 中使用p2.xlarge实例。 p2.xlarge 实例提供了具有 2,496 个 CUDA 内核的 Nvidia Tesla K80 GPU,这将使我们在本书中显示的模型的运行速度甚至比非常高端的台式计算机所能达到的速度快得多。
安装 Nvidia CUDA 工具包和 cuDNN
由于您可能会在深度学习工作中使用基于云的解决方案,因此我提供了一些说明,这些说明可帮助您在 Ubuntu Linux 上快速启动并运行,Ubuntu Linux 在各个云提供商中普遍可用。 也可以在 Windows 上安装 TensorFlow 和 Keras。 从 TensorFlow v1.2 开始,TensorFlow 不幸地不支持 OSX 上的 GPU。
在使用 GPU 之前,必须先安装 NVidia CUDA 工具包和 cuDNN 。 我们将安装 CUDA Toolkit 8.0 和 cuDNN v6.0,建议与 TensorFlow v1.4 一起使用。 在您阅读完本段之前,很有可能会发布新版本,因此,请访问 www.tensorflow.org 以获取最新的必需版本。
我们将从在 Ubuntu 上安装build-essential包开始,该包包含编译 C++ 程序所需的大部分内容。 代码在这里给出:
sudo apt-get update
sudo apt-get install build-essential
接下来,我们可以下载并安装 CUDA Toolkit。 如前所述,我们将安装 8.0 版及其相关补丁。 您可以在这个页面中找到最适合您的 CUDA 工具包。
wget https://developer.nvidia.com/compute/cuda/8.0/Prod2/local_installers/cuda_8.0.61_375.26_linux-run
sudo sh cuda_8.0.61_375.26_linux-run # Accept the EULA and choose defaults
wget https://developer.nvidia.com/compute/cuda/8.0/Prod2/patches/2/cuda_8.0.61.2_linux-run
sudo sh cuda_8.0.61.2_linux-run # Accept the EULA and choose defaults
CUDA 工具包现在应该安装在以下路径中:/usr/local/cuda。 您需要添加一些环境变量,以便 TensorFlow 可以找到它。 您可能应该考虑将这些环境变量添加到~/.bash_profile,以便在每次登录时进行设置,如以下代码所示:
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/cuda/lib64"
export CUDA_HOME="/usr/local/cuda"
此时,您可以通过执行以下命令来测试一切是否正常:nvidia-smi。 输出应类似于以下内容:
$nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 375.26 Driver Version: 375.26 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|===============================+======================+======================|
| 0 Tesla K80 Off | 0000:00:1E.0 Off | 0 |
| N/A 41C P0 57W / 149W | 0MiB / 11439MiB | 99% Default |
+-------------------------------+----------------------+----------------------+
最后,我们需要安装 cuDNN,这是 NVIDIA CUDA 深度神经网络库。
首先,将 cuDNN 下载到本地计算机。 为此,您需要在 NVIDIA 开发人员网络中注册为开发人员。 您可以在 cuDNN 主页 上找到 cuDNN。 将其下载到本地计算机后,可以使用scp将其移至 EC2 实例。 虽然确切的说明会因云提供商的不同而有所差异,但是您可以在这个页面中找到有关通过 SSH/SCP 连接到 AWS EC2 的其他信息。 。
将 cuDNN 移至 EC2 映像后,可以使用以下代码解压缩文件:
tar -xzvf cudnn-8.0-linux-x64-v6.0.tgz
最后,使用以下代码将解压缩的文件复制到其适当的位置:
sudo cp cuda/include/cudnn.h /usr/local/cuda/include/
sudo cp cuda/lib64/* /usr/local/cuda/lib64
我不清楚为什么 CUDA 和 cuDNN 分别分发,为什么 cuDNN 需要注册。 cuDNN 的下载过程和手动安装过于复杂,这确实是深度学习中最大的谜团之一。
安装 Python
我们将使用virtualenv创建一个隔离的 Python 虚拟环境。 尽管这不是严格必要的,但这是一种极好的实践。 这样,我们会将该项目的所有 Python 库保存在一个独立的隔离环境中,该环境不会干扰系统 Python 的安装。 此外,virtualenv环境将使以后打包和部署我们的深度神经网络更加容易。
首先,使用 Ubuntu 中的 aptitude 包管理器安装Python,pip和virtualenv。 以下是代码:
sudo apt-get install python3-pip python3-dev python-virtualenv
现在,我们可以为我们的工作创建虚拟环境。 我们将所有虚拟环境文件保存在名为~/deep-learn的文件夹中。 您可以自由选择该虚拟环境的任何名称。 以下代码显示了如何创建虚拟环境:
virtualenv --no-site-packages -p python3 ~/deep-learn
如果您是一位经验丰富的 Python 开发人员,您可能已经注意到我已将环境设置为默认为 Python3.x。 肯定不是必须的,并且 TensorFlow/Keras 都支持 Python 2.7。 也就是说,作者感到 Python 社区有道德义务支持现代版本的 Python。
现在已经创建了虚拟环境,您可以按以下方式激活它:
$source ~/deep-learn/bin/activate
(deep-learn)$ # notice the shell changes to indicate the virtualenv
此时,每次登录时都需要激活要使用的虚拟环境。如果您想始终输入刚刚创建的虚拟环境,可以将source命令添加到~/.bash_profile。
现在我们已经配置了虚拟环境,我们可以根据需要在其中添加 Python 包。 首先,请确保我们具有 Python 包管理器pip的最新版本:
easy_install -U pip
最后,我建议安装 IPython,它是一个交互式 Python shell,可简化开发。
pip install ipython
就是这样。 现在我们准备安装 TensorFlow 和 Keras。
安装 TensorFlow 和 Keras
在我们共同完成所有工作之后,您将很高兴看到现在安装 TensorFlow 和 Keras 多么简单。
让我们开始安装 TensorFlow
TensorFlow 的安装可以使用以下代码完成:
pip install --upgrade tensorflow-gpu
确保pip install tensorflow-gpu。 如果您通过 pip 安装 TensorfFow(不带-gpu),则将安装仅 CPU 版本。
在安装 Keras 之前,让我们测试一下 TensorFlow 安装。 为此,我将使用 TensorFlow 网站和 IPython 解释器中的一些示例代码。
通过在 bash 提示符下键入 IPython ,启动 IPython 解释程序。 IPython 启动后,让我们尝试导入 TensorFlow。 输出如下所示:
In [1]: import tensorflow as tf
In [2]:
如果导入 TensorFlow 导致错误,请对到目前为止已执行的步骤进行故障排除。 大多数情况下,当无法导入 TensorFlow 时,可能未正确安装 CUDA 或 cuDNN。
现在我们已经成功安装了 TensorFlow,我们将在 IPython 中运行一小段代码,以验证我们可以在 GPU 上运行计算:
a = tf.constant([1.0,</span> 2.0, 3.0, 4.0, 5.0, 6.0], shape=[2, 3], name='a')
b = tf.constant([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], shape=[3, 2], name='b')
c = tf.matmul(a, b)
sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))
print(sess.run(c))
如果一切顺利,我们将看到许多迹象表明正在使用我们的 GPU。 我在此处提供了一些输出,并重点介绍了提请您注意的证据。 根据硬件,您的输出可能会有所不同,但是您应该看到类似的证据,如下所示:
/job:localhost/replica:0/task:0/device:GPU:0 -> device: 0, name: Tesla K80, pci bus id: 0000:00:1e.0, compute capability: 3.7
MatMul: (MatMul): /job:localhost/replica:0/task:0/device:GPU:0
: I tensorflow/core/common_runtime/placer.cc:874] MatMul: (MatMul)/job:localhost/replica:0/task:0/device:GPU:0
b: (Const): /job:localhost/replica:0/task:0/device:GPU:0
: I tensorflow/core/common_runtime/placer.cc:874] b: (Const)/job:localhost/replica:0/task:0/device:GPU:0
a: (Const): /job:localhost/replica:0/task:0/device:GPU:0
: I tensorflow/core/common_runtime/placer.cc:874] a: (Const)/job:localhost/replica:0/task:0/device:GPU:0
[[ 22\. 28.]
[ 49\. 64.]]
在前面的输出中,我们可以看到张量a和b以及矩阵乘法运算已分配给 GPU。 如果访问 GPU 出现问题,则输出可能如下所示:
I tensorflow/core/common_runtime/placer.cc:874] b_1: (Const)/job:localhost/replica:0/task:0/device:CPU:0
a_1: (Const): /job:localhost/replica:0/task:0/device:CPU:0
I tensorflow/core/common_runtime/placer.cc:874] a_1: (Const)/job:localhost/replica:0/task:0/device:CPU:0
在这里我们可以看到张量b_1和a_1被分配给 CPU 而不是 GPU。 如果发生这种情况,说明您的 TensorFlow,CUDA 或 cuDNN 安装存在问题。
如果到目前为止,您已经安装了 TensorFlow。 剩下的唯一任务是安装 Keras。
可以在以下代码的帮助下完成 Keras 的安装:
pip install keras
就是这样! 现在我们准备在 Keras 和 TensorFlow 中构建深度神经网络。
这可能是创建快照甚至是 EC2 实例的 AMI 的好时机,因此您不必再次进行此安装。
构建用于深度学习的数据集
与您可能已经使用的其他预测模型相比,深度神经网络非常复杂。 考虑一个具有 100 个输入的网络,两个具有 30 个神经元的隐藏层以及一个逻辑输出层。 该网络将具有 3,930 个可学习的参数以及优化所需的超参数,这是一个非常小的例子。 大型卷积神经网络将具有数亿个可学习的参数。 所有这些参数使得深度神经网络在学习结构和模式方面如此惊人。 但是,这也使过度安装成为可能。
深度学习中的偏差和方差误差
您可能熟悉典型预测模型中的所谓偏差/方差折衷。 如果您不在,我们将在此处提供快速提醒。 在传统的预测模型中,当我们尝试从偏差中发现误差并从方差中发现误差时,通常会有一些折衷。 因此,让我们看看这两个误差是什么:
- 偏差误差:偏差误差是模型引入的误差。 例如,如果您尝试使用线性模型对非线性函数建模,则模型将在指定的下为,并且偏差误差会很高。
- 方差误差:方差误差是由训练数据中的随机性引起的误差。 当我们很好地拟合训练分布以至于我们的模型不再泛化时,我们就过拟合或引入了方差误差。
在大多数机器学习应用中,我们寻求找到一些折衷方案,以最小化偏差误差,同时引入尽可能小的方差误差。 我之所以这么说是因为深度神经网络的一大优点是,在很大程度上,偏差和方差可以彼此独立地进行操纵。 但是,这样做时,我们将需要非常谨慎地构造训练数据。
训练,验证和测试数据集
在本书的其余部分中,我将把我的数据分为三个独立的集合,分别称为训练,验证和测试。 从总数据集中抽取为随机样本的这三个单独的数据集的结构和大小将大致如此。
训练数据集将按预期用于训练网络。
验证数据集将用于查找理想的超参数并测量过拟合。 在周期结束时,即网络有机会观察训练集中的每个数据点时,我们将对验证集进行预测。 该预测将用于监视过拟合,并将帮助我们知道网络何时完成训练。 像这样在每个周期末尾使用验证设置与典型用法有些不同。 有关保留验证的更多信息,请参考 Hastie 和 Tibshirani 撰写的《统计学习的特征》。
一旦完成所有训练,就将使用测试数据集,以根据网络未看到的一组数据准确地测量模型表现。
验证和测试数据来自同一数据集非常重要。 训练数据集匹配验证和测试不太重要,尽管那仍然是理想的。 例如,如果使用图像增强(对训练图像进行较小的修改以尝试扩大训练集大小),则训练集分布可能不再与验证集分布匹配。 这是可以接受的,并且只要验证和测试来自同一分布,就可以充分测量网络表现。
在传统的机器学习应用中,习惯上将 10% 到 20% 的可用数据用于验证和测试。 在深度神经网络中,通常情况是我们的数据量很大,以至于我们可以用更小的验证和测试集来充分测量网络表现。 当数据量达到数以千万计的观测值时,将 98%,1%,1% 的拆分完全合适。
在深度神经网络中管理偏差和方差
现在,我们已经定义了如何构造数据并刷新偏差和方差,现在让我们考虑如何控制深度神经网络中的偏差和方差。
- 高偏差:在训练集上进行预测时,具有高偏差的网络将具有非常高的错误率。 该模型在拟合数据方面表现不佳。 为了减少偏差,您可能需要更改网络架构。 您可能需要添加层,神经元或两者。 使用卷积或循环网络可能可以更好地解决您的问题。
当然,有时由于信号不足或非常困难的问题而导致问题偏高,因此请务必以合理的速度校准您的期望(我喜欢从对人的准确率进行校准开始)。
- 高方差:具有低偏差误差的网络很好地拟合了训练数据; 但是,如果验证误差大于测试误差,则网络已开始过拟合训练数据。 减少差异的两种最佳方法是添加数据并向网络添加正则化。
添加数据很简单,但并非总是可能的。 在整本书中,我们将介绍适用的正则化技术。 我们将讨论的最常见的正则化技术是 L2 正则化,丢弃法和批量归一化。
K 折交叉验证
如果您有机器学习的经验,您可能想知道为什么我会选择通过 K 折交叉验证而不是保留(训练/验证/测试)验证。 训练深度神经网络是一项非常昂贵的操作,并且非常简单地讲,针对每个我们想探索的超参数训练 K 个神经网络通常不太实用。
我们可以确信,在给定的验证和测试集足够大的情况下,留出验证会做得很好。 在大多数情况下,我们希望在有大量数据的情况下应用深度学习,从而获得足够的值和测试集。
最终,这取决于您。 稍后我们将看到,Keras 提供了 scikit-learn 接口,该接口可将 Keras 模型集成到 scikit-learn 管道中。 这使我们能够执行 K 折,分层 K 折,甚至使用 K 折进行网格搜索。 有时在训练深层模型时使用 K 折 CV 是可行且适当的。 也就是说,在本书的其余部分中,我们将重点介绍使用留出验证。
总结
希望本章能够使您对深度神经网络架构和优化算法有所了解。 因为这是快速参考,所以我们没有做太多的详细介绍,我鼓励读者对这里可能是新手或陌生的任何材料进行更深入的研究。
我们讨论了 Keras 和 TensorFlow 的基础知识,以及为什么我们在本书中选择了这些框架。 我们还讨论了 CUDA,cuDNN,Keras 和 TensorFlow 的安装和配置。
最后,我们介绍了本书其余部分将使用的留出验证方法,以及为什么对于大多数深度神经网络应用,我们都更喜欢 K 折 CV。
当我们在以后的章节中重新审视这些主题时,我们将大量参考本章。 在下一章中,我们将开始使用 Keras 解决回归问题,这是构建深度神经网络的第一步。
二、使用深度学习解决回归问题
在本章中,我们将构建一个简单的多层感知器(MLP),它是具有单个隐藏层的神经网络的奇特名称,用于解决回归问题。 然后,我们将深入研究具有多个隐藏层的深度神经网络。 在此过程中,我们将探索模型的表现和过拟合。 所以,让我们开始吧!
我们将在本章介绍以下主题:
- 回归分析和深度神经网络
- 将深度神经网络用于回归
- 在 Keras 中建立 MLP
- 在 Keras 中建立深度神经网络
- 保存和加载经过训练的 Keras 模型
回归分析和深度神经网络
在经典回归分析中,我们使用线性模型来学习一组独立变量和因变量之间的关系。 在找到这种关系时,我们希望能够在给定自变量值的情况下预测因变量的值。
进行回归分析的第二个重要原因是要了解当所有其他自变量保持恒定时单个自变量对因变量的影响。 传统多元线性回归的一大优点是线性模型的其他条件不变属性。 我们可以通过使用与该自变量关联的学习权重来解释单个自变量对因变量的影响,而无需考虑其他自变量。 这种解释充其量是具有挑战性的,需要我们对我们的数据和模型做出很多假设。 但是,它通常非常有用。
深度神经网络很难解释,尽管尝试这样做是一个活跃的研究领域。
有关介绍深度神经网络的当前状态的介绍,请查看 Montavon 等人的《解释和理解深度神经网络的方法》。
将神经网络用于回归的好处
在本章的其余部分,我们将重点介绍使用深度神经网络进行预测。 与使用传统的多元线性回归进行比较时,您会很高兴地发现我们的神经网络具有以下优势:
- 我们不需要选择或筛选特征。 神经网络是功能强大的特征工程机器,可以了解哪些特征是相关的,而忽略了无关的特征。
- 给定足够复杂的网络,还可以学习特征交互(例如,除了
x[1]和x[2]的独立效应,x[1] * x[2]的效应)) - 您可能现在已经猜到了,我们还可以学习更高阶的多项式关系(例如
x[2]^3) - 最后,只要我们确保最终激活可以对分布进行建模,我们就不必只对正态分布建模或对非正态分布使用不同的模型。
将神经网络用于回归时要考虑的缺点
但这并不是所有的彩虹和小猫,使用神经网络解决这些真正简单的问题也有一些弊端。 最明显的缺点是:
- 如前所述,神经网络不容易解释。
- 当具有许多特征和大量数据时,神经网络最有效。 许多简单的回归问题还不够大,无法真正从神经网络中受益。
- 在很多情况下,传统的多元回归或树模型(例如梯度提升树)在此类问题上的表现将优于神经网络。 越复杂,就越适合神经网络。
将深度神经网络用于回归
既然您已经希望了解为什么(不希望)使用深度神经网络进行回归,那么我将向您展示如何做到这一点。 虽然它不像在 scikit-learn 中使用线性回归器那样简单,但我认为使用 Keras 会很容易。 最重要的是,Keras 将允许您快速迭代模型架构而无需更改大量代码。
如何规划机器学习问题
在构建新的神经网络时,我建议每次都遵循相同的基本步骤。
深度神经网络很快就会变得非常复杂。 进行一点计划和组织,大大加快您的工作流程!
以下是构建深度神经网络的步骤:
- 概述您要解决的问题。
- 确定模型的输入和输出。
- 选择
cost函数和指标。 - 创建一个初始的网络架构。
- 训练和调整网络。
定义示例问题
在我们的示例问题中,我们将使用 P. Cortez 等人创建的葡萄酒质量数据集。 考虑到白酒的其他 10 个化学特性,我们将预测白葡萄酒数据中所含酒精的百分比。
此数据集中总共有 4,898 个观测值或元素,对于经典回归问题而言可能很大,但对于深度学习问题而言却很小。
一些快速的探索性数据分析将告诉我们,我们将用来预测酒精含量的 10 个化学特征在不同尺度上都是连续变量。
加载数据集
虽然可能不是机器学习问题中最有趣的部分,但加载数据是重要的一步。 我将在这里介绍我的数据加载方法,以便您可以了解如何处理数据集。
from sklearn.preprocessing import StandardScaler
import pandas as pd
TRAIN_DATA = "./data/train/train_data.csv"
VAL_DATA = "./data/val/val_data.csv"
TEST_DATA = "./data/test/test_data.csv"
def load_data():
"""Loads train, val, and test datasets from disk"""
train = pd.read_csv(TRAIN_DATA)
val = pd.read_csv(VAL_DATA)
test = pd.read_csv(TEST_DATA)
# we will use sklearn's StandardScaler to scale our data to 0 mean, unit variance.
scaler = StandardScaler()
train = scaler.fit_transform(train)
val = scaler.transform(val)
test = scaler.transform(test)
# we will use a dict to keep all this data tidy.
data = dict()
data["train_y"] = train[:, 10]
data["train_X"] = train[:, 0:9]
data["val_y"] = val[:, 10]
data["val_X"] = val[:, 0:9]
data["test_y"] = test[:, 10]
data["test_X"] = test[:, 0:9]
# it's a good idea to keep the scaler (or at least the mean/variance) so we can unscale predictions
data["scaler"] = scaler
return data
当我从 csv,excel 甚至是 DBMS 中读取数据时,第一步通常是将其加载到 pandas 数据框中。
标准化我们的数据很重要,这样每个特征都应具有可比的范围,并且所有这些范围都应位于激活函数的范围之内。 在这里,我使用了 Scikit-Learn 的StandardScaler完成此任务。
这为我们提供了一个形状完整的数据集(4898, 10)。 我们的目标变量alcohol的百分比介于 8% 和 14.2% 之间。
在加载数据之前,我已经对数据进行了随机采样并将其划分为train,val和test数据集,因此我们在这里不必担心。
最后,load_data()函数返回一个字典,该字典将所有内容保持整齐并放在一个位置。 如果您以后看到我参考数据[X_train],则知道我正在参考训练数据集,该数据集已存储在数据字典中。
。 该项目的代码和数据均可在该书的 GitHub 网站上找到。
定义成本函数
对于回归任务,最常见的成本函数是均方根误差(RMSE)和平均绝对误差(MAE)。 我将在这里使用 MAE。 定义如下:
很简单,MAE 是数据集中所有示例的平均无符号误差。 与 RMSE 非常相似; 但是,我们使用y和y_hat之间的差的绝对值代替平均平方误差的平方根:
您可能想知道 MAE 与更熟悉的 RMSE 有何不同。 如果误差在数据集中均匀分布,则 RMSE 和 MAE 将相等。 如果数据集中有非常大的离群值,则 RMSE 将比 MAE 大得多。 您选择的成本函数应适合您的用例。 关于可解释性,MAE 比 RMSE 更具解释性,因为它是实际的平均误差。
在 Keras 中建立 MLP
Keras 使用模型对象的实例来包含神经网络。 对于熟悉 scikit-learn 的人来说,这可能是相当熟悉的。 略有不同的是 Keras 模型包含一组层。 这一组层需要由我们定义。 只需很少的代码,就可以在网络架构中实现惊人的灵活性。
Keras 当前有两个用于构建模型的 API。 在我的示例中,我将使用函数式 API。 它稍微冗长一些,但可以提供更多的灵活性。 我建议尽可能使用函数式 API。
我们的 MLP 将需要一个输入层,一个隐藏层和一个输出层。
输入层形状
由于我们已经确定了输入,因此我们知道输入矩阵的行数等于数据集中的数据元素/观测值的数量,并且列数等于变量/特征的数量。 输入矩阵的形状为(观察数量 x 10 个特征)。 TensorFlow 和 Keras 可以在定义数据集中元素的数量时使用None作为占位符,而不是定义数据集中或小批量中的确切记录数。
如果看到 Keras 或 TensorFlow 模型层形状中使用了None维度,则它实际上表示任意维度,该维度可以采用任何正整数值。
隐藏层形状
我们的隐藏层将从 32 个神经元开始。 在这一点上,我们不知道需要多少神经元。 这确实是一个超参数,以后可以进行探索和调整。 为给定问题确定合适的网络架构是深度学习领域的一个开放问题。
由于隐藏层中这 32 个神经元中的每一个都将其激活输出到输出层,因此隐藏层的形状将为(10, 32)。
输出层形状
我们的最后一层将由单个神经元组成,使用来自隐藏层的 32 个输入,将为每个观察值预测单个输出值y_hat。
将所有各层放在一起,我们的 MLP 网络结构将如下所示:
神经网络架构
现在我们已经定义了输入和输出,我们可以看一下网络的代码。
from keras.layers import Input, Dense
from keras.models import Model
def build_network(input_features=None):
inputs = Input(shape=(input_features,), name="input")
x = Dense(32, activation='relu', name="hidden")(inputs)
prediction = Dense(1, activation='linear', name="final")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='mean_absolute_error')
return model
这里的所有都是它的! 然后,我们可以使用此代码,只需调用它即可构建适合于我们问题的神经网络实例,如下所示:
model = build_network(input_features=10)
但是,在开始之前,让我们回顾一下前面代码中的一些有趣的部分:
- 每层链接到到它上面的层。 每层都是可调用的,并返回张量。 例如,当隐藏层调用它时,我们的隐藏层绑定到输入层:
x = Dense(32, activation='relu', name="hidden")(inputs)
- 我们最后一层的激活函数是线性的。 这与不使用任何激活(这是我们要进行回归)相同。
- Keras 模型需要使用
.compile()进行编译。 - 在编译调用期间,您需要定义将要使用的成本函数和优化器。 正如我们所讨论的,在此示例中,我已将 MAE 用于成本函数。 我使用具有默认参数的 Adam 作为我的优化程序,我们在第 1 章中已经介绍了这一点。很可能我们最终将希望调整 Adam 的学习速度。 这样做非常简单:您只需要定义一个自定义
adam实例,然后使用该实例即可:
from keras.optimizers import Adam
adam_optimizer = Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
model.compile(optimizer=adam_optimizer, loss='mean_absolute_error')
训练 Keras 模型
现在我们的网络已经构建和编译,剩下的就是训练它了。 就像 Python 的 scikit-learn 一样,您可以通过在模型实例上调用.fit()来做到这一点,如以下代码所示:
model.fit(x=data["train_X"], y=data["train_y"], batch_size=32, epochs=200, verbose=1, validation_data=(data["val_X"], data["val_y"]))
让我们来看一下 Keras fit方法所采用的一些重要参数。 我将假设您熟悉小批量梯度下降和训练周期,但如果不熟悉,请查看第 1 章“深度学习的基础知识”, 概述。 Keras 拟合模型中的重要参数如下:
batch_size:Keras 的默认批次大小为 32。批次大小是 Keras 将使用的迷你批次的大小。 当然,这意味着 Keras 假设您要使用小批量梯度下降。 如果由于某种原因不想使用小批量梯度,可以设置batch_size=None。epochs:一个周期只是整个训练集的单次通过。 在实践中,您需要在训练网络时对其进行监视,以了解网络何时收敛,因此epochs是一个易于学习的超参数。 稍后,我们将看到可以在每个周期甚至比最后一个周期更好的每个周期保存模型的权重。 一旦知道如何做到这一点,我们就可以选择我们认为最好的周期,并实现一种基于人的早期停止。validation_data:在这里,我们指定验证集。 在每个阶段结束时,Keras 将在验证集上测试模型,并使用损失函数和您指定的任何其他指标输出结果。 另外,您可以将validation_split设置为浮点值,以指定要用于验证的训练组的百分比。 这两个选项都可以正常工作,但是在数据集拆分方面,我希望讲得很明确。verbose:这有点不言而喻; 但是,值得一提。verbose=1输出一个进度条,显示当前周期的状态,在周期结束时,Keras 将输出训练和验证损失。 也可以将verbose设置为 2(每个小批量输出损失信息),将其设置为 0(使 Keras 保持静音)。
评估模型的表现
现在我们的 MLP 已经过训练,我们可以开始了解它的表现。 为此,我将对Train,Val和Test数据集进行预测。 相同的代码如下:
print("Model Train MAE: " + str(mean_absolute_error(data["train_y"], model.predict(data["train_X"]))))
print("Model Val MAE: " + str(mean_absolute_error(data["val_y"], model.predict(data["val_X"]))))
print("Model Test MAE: " + str(mean_absolute_error(data["test_y"], model.predict(data["test_X"]))))
对于我们的 MLP,这是我们做得如何:
Model Train MAE: 0.190074701809
Model Val MAE: 0.213255747475
Model Test MAE: 0.199885450841
请记住,我们的数据已缩放为 0 均值和单位方差。 Train MAE是0.19,而我们的Val MAE是0.21。 这两个误差彼此之间非常接近,所以过分适合并不是我太在意的事情。 因为我预计会有一些我看不到的过拟合(通常是更大的问题),所以我认为此模型可能有太多偏差。 换句话说,我们可能无法足够紧密地拟合数据。 发生这种情况时,我们需要为我们的模型添加更多的层,更多的神经元或两者。 我们需要更深入。 让我们接下来做。
我们可以尝试通过以更多神经元的形式向网络添加参数来减少网络偏差。 虽然您可能会开始尝试优化优化器,但通常最好先找到自己熟悉的网络架构。
在 Keras 中建立深度神经网络
更改模型就像重新定义我们先前的build_network()函数一样容易。 我们的输入层将保持不变,因为我们的输入没有更改。 同样,输出层应保持不变。
我将通过添加其他隐藏层将参数添加到我们的网络中。 我希望通过添加这些隐藏层,我们的网络可以了解输入和输出之间更复杂的关系。 我将从添加四个其他隐藏层开始; 前三个将具有 32 个神经元,第四个将具有 16 个神经元。其外观如下:
以下是在 Keras 中构建模型的相关代码:
def build_network(input_features=None):
inputs = Input(shape=(input_features,), name="input")
x = Dense(32, activation='relu', name="hidden1")(inputs)
x = Dense(32, activation='relu', name="hidden2")(x)
x = Dense(32, activation='relu', name="hidden3")(x)
x = Dense(32, activation='relu', name="hidden4")(x)
x = Dense(16, activation='relu', name="hidden5")(x)
prediction = Dense(1, activation='linear', name="final")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='mean_absolute_error')
return model
如所承诺的,我们的代码几乎没有改变。 我将其他行加粗了。 我们其余的代码可以保持不变。 但是,随着网络复杂性的增加,您通常必须训练更长的时间(更多的时间)。
测量深度神经网络表现
在这个问题上,深层网络真的比 MLP 好吗? 让我们找出答案! 训练了 500 个周期后,模型的效果如下:
Model Train MAE: 0.0753991873787
Model Val MAE: 0.189703853999
Model Test MAE: 0.190189985043
我们可以看到Train MAE现在从0.19减少到0.075。 我们大大降低了网络的偏差。
但是,我们的差异增加了。 训练误差和验证误差之间的差异要大得多。 我们的Val集误差确实略有下降,这很好; 但是,训练误差和验证误差之间的巨大差距表明我们开始过度适应训练集。
在这种情况下,减少差异的最直接方法是添加其他训练数据或应用诸如 L2 正则化或丢弃法之类的正则化技术,我们将在下一章中介绍。
对于高方差网络,更多的数据通常是最佳解决方案。 如果有可能收集更多数据,那可能就是花费时间的最佳位置。
建立网络后,我想直观地检查误差,以了解网络对验证集分布进行建模的程度。 这通常会带来见解,这将有助于我改进模型。 对于回归模型,我想绘制验证集的预测值和实际值的直方图。 让我们看看我的表现如何。 该图如下,供您参考:
总体而言,我认为该模型正在相当接近地预测实际分布。 似乎实际的验证数据集比预测的数据集向左移动(较小的值)要多一些,这可能是一个重要的见解。 换句话说,网络可能会预测葡萄酒的酒精含量高于平均水平,尤其是在酒精含量较低的情况下。 更仔细地检查验证数据可能会建议我们如何收集更多的训练数据。
调整模型超参数
现在,我们已经针对该问题训练了 MLP 和六层深度神经网络,现在可以调整和优化模型超参数了。
我们将在第 6 章“超参数优化”中讨论深度模型调整。 您可以使用多种策略为模型选择最佳参数。 您可能已经注意到,我们仍然可以优化许多可能的参数和超参数。
如果要完全调整此模型,则应执行以下操作:
- 试验隐藏层的数量。 看来五个可能太多,而一个可能还不够。
- 试验每个隐藏层相对于层数的神经元数量。
- 尝试添加丢弃或正则化。
- 尝试通过尝试使用 SGD 或 RMS 属性而不是 Adam 或通过对 Adam 使用不同的学习率来进一步减少模型误差。
深度神经网络有许多活动部分,有时要达到最佳状态是一个疲惫的概念。 您必须确定您的模型是否足够好。
保存和加载经过训练的 Keras 模型
您不太可能会训练一个深层的神经网络,然后将其应用到同一脚本中。 最有可能的是,您将需要训练网络,然后保存结构和权重,以便可以将其用于设计用于对新数据进行评分的面向生产的应用中。 为此,您需要能够保存和加载模型。
在 Keras 中保存模型非常简单。 您可以使用模型实例的.save()方法将网络结构和权重保存到hdf5文件,如以下代码所示:
model.save("regression_model.h5")
这就是全部。 从磁盘加载模型非常简单。 此处提供了执行此操作的代码供您参考:
from keras.models import load_model
model = load_model("regression_model.h5")
总结
当您考虑深度学习时,您可能会想到令人印象深刻的复杂计算机视觉问题,但是即使对于像这样的简单回归问题,深度神经网络也可能有用。 希望我已经证明了这一点,同时还介绍了 Keras 语法并向您展示了如何构建一个非常简单的网络。
随着我们的继续,我们将遇到更多的复杂性。 更大的网络,更复杂的成本函数以及高维输入数据。 但是,我在本章中使用的过程在大多数情况下将保持不变。 在每种情况下,我们都将概述问题,确定输入和输出,选择成本函数,创建网络架构,最后训练和调整模型。
如果考虑以下因素,则在深度神经网络中通常可以独立地控制和减少偏差和方差:
- 偏差:可以通过增加模型复杂度来减少此偏差。 其他神经元或层将有所帮助。 添加数据并不能真正帮助减少偏差。
- 方差:可以通过添加数据或正则化来减少此变化。
在下一章中,我们将讨论如何使用 TensorBoard 更快地对深度神经网络进行优化和故障排除。
三、使用 TensorBoard 监控网络训练
在本章中,我将向您展示如何使用 TensorBoard 帮助更快更轻松地训练深度神经网络。 我认为 TensorBoard 是一个很棒的工具,经常被忽略,而它又常常被拖到脚注或上一章中。 现在,让我们看一下 TensorBoard,以便我们可以立即开始利用它。
我们将在本章介绍以下主题:
- TensorBoard 的简要概述
- 设置 TensorBoard
- 将 Keras 连接到 TensorBoard
- 使用 TensorBoard
TensorBoard 的简要概述
TensorBoard 是一个基于 Web 的应用,可以帮助您可视化 TensorFlow 中创建的深度神经网络的指标,参数和结构。 它将帮助您更快,更轻松地调试和优化深度神经网络。
正如您现在可能已经猜到的那样,深度神经网络可能变得相当复杂。 不幸的是,这意味着很多事情可能出错。 众所周知,我时不时地会犯一个错误,而当错误发生在一个深度神经网络内部时,该深度神经网络位于一个框架内,该框架在另一个框架上运行,在一个 GPU 上运行,很难找到这些错误。 他们。 TensorBoard 可能是您需要在其他本来很暗的房间中发现问题的手电筒。 TensorBoard 将允许您在训练网络时监视指标和参数的变化,这可以大大加快故障排除速度。
TensorBoard 也非常适合优化。 借助 TensorBoard,您可以直观地比较多个模型运行。 这使您可以试验不断变化的架构和超参数,然后相对于网络的其他运行评估那些变化。 所有这一切都可能在每个周期发生,因此如果您愿意,您可以取消效果不佳的模型运行,从而节省了时间和金钱。 您可以在这个页面上阅读有关 TensorBoard 的更多信息。
设置 TensorBoard
TensorBoard 是一个独立的 Web 应用。 您将通过网络浏览器使用它。 设置需要两个步骤。 首先,我们将设置 TensorBoard 以可视化在 TensorFlow 和 Keras 中构建的网络,然后我们将设置 Keras 与 TensorBoard 共享信息。
本节介绍 TensorBoard 的设置。 接下来的内容将涉及修改 Keras 代码以与 TensorBoard 共享信息。
安装 TensorBoard
如果您已经安装了 TensorFlow,则您的机器上可能已经安装了 Tensorboard。 万一您可以安装和更新 TensorBoard,可以使用pip进行安装,就像 Keras 和 TensorFlow 一样。 要安装它,只需运行以下命令:
pip install -U tensorboard
TensorBoard 如何与 Keras/TensorFlow 交互
TensorBoard 和 TensorFlow 使用公共日志目录共享信息。 在 Keras 和 TensorFlow 训练中,Keras 将指标和激活直方图(稍后将对此进行详细介绍)写入您指定的日志目录中。 现在,让我们使用以下代码在主目录中为该示例创建一个日志目录:
mkdir ~/ch3_tb_log
运行 TensorBoard
剩下的就是启动 TensorBoard 进程。 我们可以使用以下代码启动 TensorBoard:
tensorboard --logdir ~/ch3_tb_log --port 6006
您可能已经猜到了,--logdir指定我们刚刚创建的目录,--port 6006指定 TensorBoard 将在其上运行的端口。 端口6006是默认端口。 但是,您可以使用所需的任何端口。
现在,您应该可以通过将浏览器指向http://<ip address>:6006来导航到 TensorBoard URL。
如果使用的是云服务,则可能还需要调整防火墙或安全规则,以允许通过端口6006连接到服务器。 在 Amazon Web Services(AWS)上,您可以通过编辑与您的 EC2 实例关联的安全组中的入站规则来执行此操作:
您可能不希望像我上面那样允许全世界范围内的开放访问。 这只是一个测试实例,因此我不太关心安全性,无论如何我都喜欢过着危险的生活。
如果一切正常,您应该看到一个空的 TensorBoard,如下所示:
不用担心,我们很快就会填满。
将 Keras 连接到 TensorBoard
现在 TensorBoard 已启动并正在运行,剩下的就是告诉 Keras 将 TensorBoard 日志写入我们上面指定的目录。 幸运的是,这确实很容易实现,它为我们提供了一个很好的机会来了解 Keras 中称为 Keras 回调的特殊函数类。
引入 Keras 回调
Keras 中的回调是可以在训练过程中运行的函数。 他们可以做各种伟大的事情,例如在某个周期之后节省模型权重,记录事情,更改超参数或方便地编写 TensorBoard 日志文件。 您甚至可以创建自己的自定义回调。
在下一节中,我们将使用 TensorBoard 回调。 但是,我鼓励您在这个页面上查看 Keras 中可用的所有回调。
TensorBoard 回调是可以在模型训练之前进行配置和实例化的对象。 我们将创建这些回调的列表。 一旦创建了要用于深度神经网络的回调列表,我们就可以将该列表作为参数传递给模型的.fit()方法。 然后,将在每个周期或 Keras 适当时使用这些回调。 在我们继续下一个示例时,这将更有意义。
创建一个 TensorBoard 回调
在本章中,我通过复制第 2 章“开始使用深度学习来解决回归问题”的网络和数据。 我们将做一些简单的添加来添加 TensorBoard 回调。 让我们从修改我们首先构建的mlp开始。
首先,我们需要使用以下代码导入 TensorBoard 回调类:
from keras.callbacks import TensorBoard
然后,我们将启动回调。 我喜欢在创建所有回调的函数中执行此操作,以使事情精心制作和整理。 下面的create_callbacks()函数将返回我们将传递给.fit()的所有回调的列表。 在这种情况下,它将返回一个包含一个元素的列表:
def create_callbacks():
tensorboard_callback = TensorBoard(log_dir='~/ch3_tb_log/mlp',
histogram_freq=1, batch_size=32, write_graph=True,
write_grads=False)
return [tensorboard_callback]
在继续之前,我们先介绍一下这里使用的一些参数:
log_dir**:**这是我们将为 TensorBoard 写入日志文件的路径。
您可能已经注意到,我正在将 MLP 网络的 TensorBoard 回调的日志写入~/ch_3_tb_log/mlp,这将在我们为 TensorBoard 指定的目录下创建一个新的目录mlp。 这是故意的。 我们将配置在第 2 章,“使用深度学习解决回归问题”训练的深度神经网络模型,以登录到单独的目录~/ch_3_tb_log/dnn。 这样做将使我们能够比较两个模型的运行。
histogram_freq:这指定我们将多长时间计算一次激活和权重的直方图(以周期为单位)。 它的默认值为 0,这会使日志更小,但不会生成直方图。 我们将介绍为什么以及何时您会对直方图感兴趣。batch_size:这是用于计算直方图的批量大小。 默认为 32。write_graph:此函数为布尔值。 这将告诉 TensorBoard 可视化网络图。 这可能非常方便,但也会使日志变得很大。write_grads:此函数也是布尔值。 这将告诉 TensorBoard 也计算梯度的直方图。
由于 TensorFlow 会自动为您计算梯度,因此很少使用。 但是,如果您要使用自定义激活或费用,它可能是出色的故障排除工具。
TensorBoard 回调可以接受用于在图像上运行神经网络或通过使用嵌入式层的其他参数。 我们将在本书的后面介绍这两个方面。 如果您对这些函数感兴趣,请访问 TensorBoard API 文档。
现在,我们只需要创建回调列表,并将mlp与callbacks参数匹配即可。 看起来像这样:
callbacks = create_callbacks()
model.fit(x=data["train_X"], y=data["train_y"], batch_size=32,
epochs=200, verbose=1, validation_data=(data["val_X"],
data["val_y"]), callbacks=callbacks)
为了清楚起见,我将新参数加粗了。
在继续使用 TensorBoard 之前,我将以与检测mlp相同的方式来检测深度神经网络。 唯一的代码更改是我们将 TensorBoard 日志写入的目录。 下面给出了实现该方法的方法,供您参考:
def create_callbacks():
tensorboard_callback = TensorBoard(log_dir='./ch3_tb_log/dnn',
histogram_freq=1, batch_size=32, write_graph=True, write_grads=False)
return [tensorboard_callback]
其余代码将相同。 现在,让我们再次训练每个网络,看看 TensorBoard。
使用 TensorBoard
现在我们已经完全配置了 TensorBoard 并告诉我们的网络如何向其发送日志数据,我们可以开始利用它了。 在本章的其余部分,我将向您展示一些我最喜欢的使用 TensorBoard 的方式。 TensorBoard 的功能不只此而已,我们将在本书的其余部分中重新讨论其他功能。
可视化训练
由于我们已在第 2 章“使用了深度学习解决回归问题”中使用这两种模型编写了日志数据,因此可以使用 TensorBoard 以图形方式比较这两种模型。 打开 TensorBoard 并转到SCALARS选项卡。 您应该会看到类似这样的内容。 您可能需要单击loss和val_loss来展开图形:
张量板显示模型的损失图和val_loss图
如果您查看屏幕的左下角,则应注意,我们创建的每个目录都有与之关联的运行。 两者均处于选中状态。 这意味着在我们的图形上,我们将看到两个模型的输出。
TensorBoard 可以容纳许多运行,并且您可以通过正则表达式过滤它们(例如^dnn将显示所有以dnn开头的运行)。 这意味着,如果您通过许多实验或运行(例如超参数优化)来搜索最佳模型,则可以在明确并一致地命名运行,并包含有意义的超参数和架构信息的情况下,以这个名字快速浏览它们!
这些图上的默认 X 比例尺是周期。 Y 值是我们选择的损失函数,即 MAE。 您可以单击图形以浏览它们并拖动以缩放。
看到这样的图,我们真的可以看到每个网络的相对偏差和方差。 虽然模型之间在训练损失方面有很好的分离,但深度神经网络在验证集上只得到了一点点改善,这表明我们已经进入了过拟合的领域。
可视化网络图
虽然能够查看我们的训练过程并比较模型显然很不错,但这并不是 TensorBoard 所能做的。 我们还可以使用它来可视化网络结构。 在这里,我导航到GRAPHS并提出了深度神经网络的结构:
TensorBoard 显示深度神经网络的结构
训练节点代表输入张量,默认情况下,正是这个巨型章鱼以某种无益的方式连接到图的其余部分。 要解决此问题,您只需单击该节点,然后单击从主图中删除。 然后将其移到侧面。
可视化损坏的网络
TensorBoard 是一个出色的故障排除工具。 为了证明这一点,我将复制我们的深度神经网络并将其破坏! 幸运的是,打破神经网络真的很容易。 相信我,我已经无意间做了这件事,以至于我现在基本上是专家。
想象一下,您刚刚训练了一个新的神经网络,并且看到损失看起来像这样:
该网络的损失函数被卡住,并且比我们之前的运行要高得多。 什么地方出了错?
导航到 TensorBoard 的HISTOGRAMS部分,并可视化第一个隐藏层。 让我们比较两个网络中隐藏层 1 的权重直方图:
显示两个网络中隐藏层 1 的权重直方图的屏幕截图
对于标记为 dnn 的网络的偏差和权重,您将看到权重分布在整个图中。 您甚至可以说每个分布都可能是正态分布。
您也可以在“分布”部分比较权重和偏差。 两者都以略有不同的方式呈现大多数相同的信息。
现在,看看我们破碎的网络的权重和偏置。 并不是这样分散,实际上的权重基本上是相同的。 网络并不是真正的学习。 该层中的每个神经元看起来或多或少都是相同的。 如果您查看其他隐藏层,则会看到更多相同的层。
您可能想知道我是怎么做到的。 您很幸运,我会分享我的秘密。 毕竟,您永远都不知道何时需要断开自己的网络。 为了解决问题,我将网络中的每个神经元初始化为完全相同的值。 发生这种情况时,每个神经元在反向传播期间收到的误差是完全相同的,并且更改的方式也完全相同。 网络然后无法破坏对称性。 以随机方式将权重初始化到深度神经网络非常重要,如果您违反了该规则,就会发生这种情况!
遇到问题时,可以像这样完全使用 TensorBoard。 请记住,我们的深度神经网络有 4033,在深度学习领域中,它仍然可以算作很小的。 使用 TensorBoard,我们能够直观地检查 4033 个参数并确定问题。 TensorBoard 是一个用于深度学习的暗室中的神奇手电筒。
总结
在本章中,我们讨论了如何安装,配置和使用 TensorBoard。 我们讨论了如何使用 TensorBoard 在 TensorBoard 的SCALARS部分中的每个周期检查模型的损失函数,从而直观地比较模型。 然后,我们使用 TensorsBoard 的GRAPHS部分来可视化网络结构。 最后,我们通过查看直方图向您展示了如何使用 TensorBoard 进行故障排除。
在下一章中,我们将研究如何使用 Keras 和 TensorFlow 解决二分类问题,从而扩展我们的深度学习技巧。
四、使用深度学习解决二分类问题
在本章中,我们将使用 Keras 和 TensorFlow 解决棘手的二分类问题。 我们将首先讨论深度学习对此类问题的利弊,然后我们将继续使用与第 2 章“学习解决回归问题”中使用的相同框架建立解决方案。 最后,我们将更深入地介绍 Keras 回调,甚至使用自定义回调来实现每个周期的受试者工作特征的曲线下面积(ROC AUC)指标。
我们将在本章介绍以下主题:
- 二分类和深度神经网络
- 案例研究 – 癫痫发作识别
- 在 Keras 中建立二分类器
- 在 Keras 中使用检查点回调
- 在自定义回调中测量 ROC AUC
- 测量精度,召回率和 f1 得分
二分类和深度神经网络
二分类问题(例如回归问题)是非常常见的机器学习任务。 如此之多,以至于任何一本有关深度学习的书都无法完整覆盖。 可以肯定的是,我们还没有真正达到深度神经网络的甜蜜点,但是我们进展顺利。 在开始编写代码之前,让我们谈谈在选择深度神经网络来解决此类问题时应考虑的权衡。
深度神经网络的好处
与更传统的分类器(例如逻辑回归模型)或什至基于树的模型(例如随机森林或梯度提升机)相比,深度神经网络有一些不错的优点。
与回归一样,在第 2 章“使用深度学习解决回归问题”中,我们不需要选择或筛选特征。 在本章选择的问题中,有 178 个输入变量。 每个输入变量都是来自标记为x1..x178的脑电图(EEG)的特定输入。 即使您是医生,也很难理解这么多特征与目标变量之间的关系。 这些特征中的某些特征很可能是不相关的,而这些变量和目标之间可能存在一些更高级别的交互,这是一个更好的机会。 如果使用传统模型,则经过特征选择步骤后,我们将获得最佳模型表现。 使用深度神经网络时不需要这样做。
深度神经网络的缺点
正如我们在第 2 章“使用深度学习解决回归问题”所述,深度神经网络不容易解释。 虽然深度神经网络是出色的预测器,但要理解它们为何得出自己的预测并不容易。 需要重复的是,当任务是要了解哪些特征与目标的变化最相关时,深度神经网络并不是工作的工具。 但是,如果目标是原始预测能力,则应考虑使用深度神经网络。
我们还应该考虑复杂性。 深度神经网络是具有许多参数的复杂模型。 找到最佳的神经网络可能需要花费时间和实验。 并非所有问题都能确保达到如此复杂的水平。
在现实生活中,我很少使用深度学习作为结构化数据问题的第一个解决方案。 我将从可能可行的最简单模型开始,然后根据问题的需要迭代进行深度学习。 当问题域包含图像,音频或文本时,我更有可能从深度学习开始。
案例研究 – 癫痫发作识别
您可能已经猜到了,我们将要解决二分类问题。 我们将使用与在第 2 章“使用深度学习解决回归问题”建立的框架相同的框架来计划问题,并根据需要对其进行修改。 您可以在本书的 GitHub 存储库中的第 4 章“使用深度学习解决回归问题”,找到本章的完整代码。
定义我们的数据集
我们将在本章中使用的数据集称为癫痫发作识别数据集。 数据最初来自Andrzejak RG 等人在 Phys 上发表的论文《指示脑电活动的时间序列中的非线性确定性和有限维结构:对记录区域和大脑状态的依赖性》。您可以在 UCI 机器学习存储库中找到数据。
我们的目标是创建一个深度神经网络,根据输入特征,该网络可以预测患者是否有癫痫发作。
加载数据
我们可以使用以下函数加载本章中使用的数据。 它与我们在第 2 章中使用的函数非常相似,但是适用于此数据集。
from sklearn.preprocessing import StandardScaler
def load_data():
"""Loads train, val, and test datasets from disk"""
train = pd.read_csv(TRAIN_DATA)
val = pd.read_csv(VAL_DATA)
test = pd.read_csv(TEST_DATA)
# we will use a dict to keep all this data tidy.
data = dict()
data["train_y"] = train.pop('y')
data["val_y"] = val.pop('y')
data["test_y"] = test.pop('y')
# we will use sklearn's StandardScaler to scale our data to 0 mean, unit variance.
scaler = StandardScaler()
train = scaler.fit_transform(train)
val = scaler.transform(val)
test = scaler.transform(test)
data["train_X"] = train
data["val_X"] = val
data["test_X"] = test
# it's a good idea to keep the scaler (or at least the mean/variance) so we can unscale predictions
data["scaler"] = scaler
return data
模型输入和输出
该数据集中有 11,500 行。 数据集的每一行包含 178 个数据点,每个数据点代表 1 秒钟的 EEG 记录样本和相应的患者状态,跨 100 个不同患者生成。
数据集中有五个患者状态。 但是,状态 2 至状态 5 的患者未发生癫痫发作。 状态 1 的患者正在发作。
我已经修改了原始数据集,通过将状态 2-5 更改为 0 级(表示无癫痫发作)和将 1 级(表示有癫痫发作)将状态重新定义为二分类问题。
与第 2 章“使用深度学习解决回归问题”中的回归问题一样,我们将使用 80% 的训练,10% 的 val,10% 的测试分割。
成本函数
我们需要分类器来预测癫痫发作的可能性,即类别 1。这意味着我们的输出将被限制为[0, 1],就像在传统的逻辑回归模型中一样。 在这种情况下,我们的成本函数将是二元交叉熵,也称为对数损失。 如果您以前使用过分类器,那么您可能很熟悉此数学运算; 但是,作为复习,我将在这里包括。
对数损失的完整公式如下所示:
这可能更简单地看作是两个函数的集合,对于情况y[i] = 0和y[i] = 1,一个函数:
当y[i] = 1,
当y[i] = 0。
对数函数在这里用于产生单调函数(一个一直在增加或减少的函数),我们可以轻松微分它。 与所有成本函数一样,我们将调整网络参数以最小化网络成本。
使用指标评估表现
除了loss函数之外,Keras 还使我们可以使用度量标准来帮助判断模型的表现。 虽然最大程度地降低损失是有好处的,但在给定loss函数的情况下,我们如何期望模型执行效果并不是特别明显。 度量标准并不用于训练模型,它们只是用来帮助我们了解当前状态。
尽管损失对我们而言并不重要,但准确率却对我们而言意义重大。 我们人类非常了解准确率。
Keras 定义二元精度如下:
def binary_accuracy(y_true, y_pred):
return K.mean(K.equal(y_true, K.round(y_pred)), axis=-1)
这实际上只是将正确答案的数量除以总答案的一种聪明方法,这是我们自从上学初期就可能一直在做的一项工作,目的是计算出考试的成绩。
您可能想知道我们的数据集是否平衡,因为准确率对于不平衡的数据集而言效果很差。 实际上这是不平衡的。 只有五分之一的数据集是类 1。我们将 ROC AUC 分数作为自定义回调来计算,以解决此问题。 在 Keras 中未将 ROC 用作度量标准,因为度量标准是针对每个小型批次计算的,并且 ROC AUC 分数并非真正由小型批次定义。
在 Keras 中建立二分类器
既然我们已经定义了问题,输入,期望的输出和成本函数,我们就可以在 Keras 中快速编写其余代码。 我们唯一缺少的是网络架构。 我们将很快讨论更多。 关于 Keras 的我最喜欢的事情之一是调整网络架构有多么容易。 如您所见,在找到最佳架构之前,可能需要进行大量实验。 如果是这样,那么易于更改的框架会使您的工作变得更加轻松!
输入层
和以前一样,我们的输入层需要知道数据集的维度。 我喜欢在一个函数中构建整个 Keras 模型,并允许该函数传递回已编译的模型。 现在,此函数仅接受一个参数,即特征数。 以下代码用于定义输入层:
def build_network(input_features=None):
# first we specify an input layer, with a shape == features
inputs = Input(shape=(input_features,), name="input")
隐藏层
我们已经定义了输入,这很容易。 现在我们需要确定网络架构。 我们如何知道应该包括多少层以及应该包含多少个神经元? 我想给你一个公式。 我真的会。 不幸的是,它不存在。 实际上,有些人正在尝试构建可以学习其他神经网络的最佳架构的神经网络。 对于我们其余的人,我们将不得不尝试,寻找自己或借用别人的架构。
如果我们使用的神经元过多会怎样?
如果我们使网络架构过于复杂,则会发生两件事:
- 我们可能会开发一个高方差模型
- 该模型将比不太复杂的模型训练得慢
如果我们增加许多层,我们的梯度将变得越来越小,直到前几层几乎没有训练为止,这就是梯度消失问题。 我们离那还很遥远,但是我们稍后会讨论。
用说唱传奇克里斯托弗·华莱士(又名臭名昭著的 B.I.G.)的话来说,我们遇到的神经元越多,看到的问题就越多。 话虽如此,方差可以通过丢弃法,正则化和提早停止进行管理,GPU 计算的进步使更深层次的网络成为可能。
如果我必须在神经元太多或太少的网络之间进行选择,而我只能尝试一个实验,那么我宁愿选择稍微过多的神经元。
如果我们使用的神经元太少会怎样?
想象一下,我们没有隐藏层,只有输入和输出的情况。 我们在第 1 章“深度学习的基础知识”中讨论了该架构,在此我们展示了如何无法为XOR函数建模。 这样的网络架构无法对数据中的任何非线性进行建模,因此无法通过网络进行建模。 每个隐藏层都为特征工程越来越复杂的交互提供了机会。
如果选择的神经元太少,则结果可能如下:
- 真正快速的神经网络
- 那有很高的偏差,而且预测不是很好
选择隐藏层架构
因此,既然我们了解选择太多参数而不是选择太多参数的价格和行为,那么从哪里开始呢? 据我所知,剩下的只是实验。
测量这些实验可能很棘手。 如果像我们的早期网络一样,您的网络训练很快,那么可以在多种架构中实现诸如交叉验证之类的东西,以评估每种架构的多次运行。 如果您的网络需要很长时间进行训练,则可能会留下一些统计上不太复杂的信息。 我们将在第 6 章“超参数优化”中介绍网络优化。
一些书籍提供了选择神经网络架构的经验法则。 我对此表示怀疑和怀疑,您当然不会在这里找到一个。
为我们的示例编码隐藏层
对于我们的示例问题,我将使用五个隐藏层,因为我认为特征之间存在许多交互。 我的直觉主要基于领域知识。 阅读数据描述后,我知道这是时间序列的横截面切片,并且可能是自动相关的。
我将从第一层的 128 个神经元开始(略小于我的输入大小),然后在接近输出时减半到 16 个神经元。 这完全不是凭经验,它仅基于我自己的经验。 我们将使用以下代码定义隐藏层:
x = Dense(128, activation='relu', name="hidden1")(inputs)
x = Dense(64, activation='relu', name="hidden2")(x)
x = Dense(64, activation='relu', name="hidden3")(x)
x = Dense(32, activation='relu', name="hidden4")(x)
x = Dense(16, activation='relu', name="hidden5")(x)
在每一层中,我都使用relu激活,因为它通常是最好和最安全的选择,但是要确保这也是可以试验的超参数。
输出层
最后,我们需要网络的输出层。 我们将使用以下代码定义输出层:
prediction = Dense(1, activation='sigmoid', name="final")(x)
在此示例中,我们正在构建一个二分类器,因此我们希望我们的网络输出观察结果属于类 1 的概率。幸运的是,sigmoid激活将精确地做到这一点,将网络输出限制在 0 到 1 之间。
放在一起
将所有代码放在一起,剩下的就是编译我们的 Keras 模型,将binary_crossentrophy指定为我们的loss函数,将accuracy指定为我们希望在训练过程中监控的指标。 我们将使用以下代码来编译我们的 Keras 模型:
def build_network(input_features=None):
inputs = Input(shape=(input_features,), name="input")
x = Dense(128, activation='relu', name="hidden1")(inputs)
x = Dense(64, activation='relu', name="hidden2")(x)
x = Dense(64, activation='relu', name="hidden3")(x)
x = Dense(32, activation='relu', name="hidden4")(x)
x = Dense(16, activation='relu', name="hidden5")(x)
prediction = Dense(1, activation='sigmoid', name="final")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='binary_crossentropy',
metrics=["accuracy"])
return model
训练我们的模型
现在我们已经定义了模型,我们都准备对其进行训练。 我们的操作方法如下:
input_features = data["train_X"].shape[1]
model = build_network(input_features=input_features)
model.fit(x=data["train_X"], y=data["train_y"], batch_size=32, epochs=20, verbose=1, validation_data=(data["val_X"], data["val_y"]), callbacks=callbacks)
如果您已经阅读第 2 章“使用深度学习解决回归问题”,则应该看起来很熟悉。 在大多数情况下,实际上是相同的。 回调列表包含 TensorBoard 回调,因此让我们观看我们的网络训练 20 个周期,看看会发生什么:
尽管我们的训练损失继续下降,但我们可以看到val_loss到处都在跳跃。 大约在第八个周期之后,我们就过拟合了。
有几种方法可以减少网络差异并管理这种过拟合,下一章将介绍大多数方法。 但是,在开始之前,我想向您展示一些有用的东西,称为检查点回调。
在 Keras 中使用检查点回调
在第 2 章“使用深度学习解决回归问题”中,我们看到了.save()方法,该方法使我们可以在完成训练后保存 Keras 模型。 但是,如果我们可以不时地将权重写入磁盘,以便在上一个示例中及时返回,并在模型开始过拟合之前保存其版本,那不好吗? 然后,我们可以就此停止,并使用网络的最低方差版本。
这正是ModelCheckpoint回调为我们所做的。 让我们来看看:
checkpoint_callback = ModelCheckpoint(filepath="./model-weights.{epoch:02d}-{val_acc:.6f}.hdf5", monitor='val_acc', verbose=1, save_best_only=True)
ModelCheckpoint将为我们执行的工作是按计划的时间间隔保存模型。 在这里,我们告诉ModelCheckpoint每当我们达到新的最佳验证精度(val_acc)时都要保存模型的副本。 我们也可以监视验证损失或我们指定的任何其他指标。
文件名字符串将包含周期编号和运行的验证准确率。
当我们再次训练模型时,我们可以看到正在创建以下文件:
model-weights.00-0.971304.hdf5
model-weights.02-0.977391.hdf5
model-weights.05-0.985217.hdf5
因此,我们可以看到在第 5 个阶段之后,我们无法达到val_acc的最佳水平,并且没有编写检查点。 然后,我们可以返回并从检查点 5 加载权重,并使用最佳模型。
这里有一些大的假设,将第 5 期称为最佳。 您可能需要多次运行网络,尤其是在您的数据集相对较小的情况下,就像本书中的早期示例一样。 我们可以肯定地说,这个结果将是不稳定的。
顺便说一下,这是防止过拟合的非常简单的方法。 我们可以选择使用方差太大之前发生的模型检查点。 这是做类似提前停止的一种方法,这意味着当我们看到模型没有改善时,我们会在指定的周期数之前停止训练。
在自定义回调中测量 ROC AUC
让我们再使用一个回调。 这次,我们将构建一个自定义的回调,以在每个周期结束时在训练集和测试集上计算曲线下的接收器工作特征区域(ROC AUC)。
在 Keras 中创建自定义回调实际上非常简单。 我们需要做的就是创建一个固有的Callback类,并覆盖所需的方法。 由于我们想在每个周期结束时计算 ROC AUC 分数,因此我们将在_epoch_end上覆盖:
from keras.callbacks import Callback
class RocAUCScore(Callback):
def __init__(self, training_data, validation_data):
self.x = training_data[0]
self.y = training_data[1]
self.x_val = validation_data[0]
self.y_val = validation_data[1]
super(RocAUCScore, self).__init__()
def on_epoch_end(self, epoch, logs={}):
y_pred = self.model.predict(self.x)
roc = roc_auc_score(self.y, y_pred)
y_pred_val = self.model.predict(self.x_val)
roc_val = roc_auc_score(self.y_val, y_pred_val)
print('\n *** ROC AUC Score: %s - roc-auc_val: %s ***' %
(str(roc), str(roc_val)))
return
现在,我们已经创建了新的自定义回调,我们可以将其添加到回调创建器函数中,如以下代码所示:
def create_callbacks(data):
tensorboard_callback = TensorBoard(log_dir=os.path.join(os.getcwd(),
"tb_log", "5h_adam_20epochs"), histogram_freq=1, batch_size=32,
write_graph=True, write_grads=False)
roc_auc_callback = RocAUCScore(training_data=(data["train_X"],
data["train_y"]), validation_data=(data["val_X"], data["val_y"]))
checkpoint_callback = ModelCheckpoint(filepath="./model-weights.
{epoch:02d}-{val_acc:.6f}.hdf5", monitor='val_acc',verbose=1,
save_best_only=True)
return [tensorboard_callback, roc_auc_callback, checkpoint_callback]
这里的所有都是它的! 您可以用相同的方式实现其他任何指标。
测量精度,召回率和 f1 得分
正如您可能对其他二分类器有丰富的经验一样,我认为用几句话讨论如何创建与更传统的二分类器一起使用的一些常规指标是明智的。
Keras 函数式 API 与 scikit-learn 中可能使用的 API 之间的区别是.predict()方法的行为。 当使用 Keras 时,对于n个样本中的每个,.predict()将返回k类概率的nxk矩阵。 对于二分类器,将只有一列,即类别 1 的类别概率。这使 Keras .predict()更像 scikit-learn 中的.predict_proba()。
在计算精度,召回率或其他基于类的指标时,您需要通过选择一些操作点来转换.predict()输出,如以下代码所示:
def class_from_prob(x, operating_point=0.5):
x[x >= operating_point] = 1
x[x < operating_point] = 0
return x
完成此操作后,您可以随意重用sklearn.metric中的典型指标,如以下代码所示:
y_prob_val = model.predict(data["val_X"])
y_hat_val = class_from_prob(y_prob_val)
print(classification_report(data["val_y"], y_hat_val))
总结
在本章中,我们讨论了使用深度神经网络作为二分类器。 我们花了很多时间讨论网络架构的设计选择,并提出了这样的想法,即搜索和试验是当前选择架构的最佳方法。
我们学习了如何在 Keras 中使用检查点回调来使我们能够及时返回并找到具有所需表现特征的模型版本。 然后,我们在训练的模型中创建并使用了自定义回调来衡量 ROC AUC 得分。 我们总结了如何将 Keras .predict()方法与sklearn.metrics中的传统指标结合使用。
在下一章中,我们将研究多分类,我们将更多地讨论如何防止过拟合。
五、使用 Keras 解决多分类问题
在本章中,我们将使用 Keras 和 TensorFlow 来处理具有许多自变量的 10 类多分类问题。 和以前一样,我们将讨论使用深度学习解决此问题的利弊; 但是,您不会发现很多缺点。 最后,我们将花费大量时间讨论控制过拟合的方法。
我们将在本章介绍以下主题:
- 多分类和深度神经网络
- 案例研究 – 手写数字分类
- 在 Keras 中建立多分类器
- 通过丢弃控制方差
- 通过正则化控制方差
多分类和深度神经网络
这里是! 我们终于找到了有趣的东西! 在本章中,我们将创建一个深度神经网络,该网络可以将观察结果分类为多个类别,这是神经网络确实发挥出色的地方之一。 让我们再谈一些关于深度神经网络对此类问题的好处。
就像我们都在谈论同一件事一样,让我们在开始之前定义多分类。 想象我们有一个分类器,该分类器将各种水果的权重作为输入,并根据给定的权重来预测水果。 输出可能恰好是一组类(苹果,香蕉,芒果等)中的一个类。 这是多分类,不要与多标签混淆,在这种情况下,模型可能会预测一组标签是否将应用于互不排斥的观察结果。
优点
当我们需要预测大量类时,相对于其他模型,深度神经网络的确是出色的执行者。 当输入向量中的特征数量变大时,神经网络自然适合。 当这两种情况都集中在同一个问题上时,我可能就是从那里开始的。 这正是我们将在本章中要研究的案例研究中看到的问题的类型。
缺点
和以前一样,更简单的模型可能会比深度学习模型做的更好或更好。 在所有其他条件都相同的情况下,您可能应该支持更简单的模型。 但是,随着类数的增加,深度神经网络复杂性的弊端通常会减少。 为了容纳许多类,许多其他模型的实现必须变得非常复杂,有些模型甚至可能需要优化作为超参数用于模型的多类策略。
案例研究 - 手写数字分类
我们将使用多分类网络来识别手写数字的相应类。 与以前一样,如果您想继续阅读,可以在本书的 Git 存储库中的Chapter05下找到本章的完整代码。
问题定义
MNIST数据集已成为几乎规范的神经网络数据集。 该数据集由 60,000 个手写数字组成的图像,属于代表它们各自数字(0, 1, 2 ... 9)的 10 类。 由于此数据集变得如此普遍,因此许多深度学习框架都在 API 中内置了 MNIST 加载方法。 TensorFlow 和 Keras 都拥有一个,我们将使用 Keras MNIST 加载器使我们的生活更轻松。 但是,如果您想从原始数据中获取数据,或者想进一步了解 MNIST 的历史,可以在这个页面中找到更多信息。
模型输入和输出
我们的数据集已被划分为一个训练集,该训练集的大小为 50,000 个观察值,一个测试集为 10,000 个观察值。 我将从训练集中获取最后 5,000 个观察值,并将其用作验证集。
拼合输入
每个输入观察都是一个 28 像素乘 28 像素的黑白图像。 像这样的一幅图像在磁盘上表示为28x28的矩阵,其值介于 0 到 255 之间,其中每个值都是该像素中黑色的强度。 至此,我们只知道如何在二维向量上训练网络(稍后我们将学习一种更好的方法); 因此我们将这个28x28矩阵展平为1 x 784输入向量。
一旦我们堆叠了所有这些1x784向量,就剩下50,000 x 784训练集。
如果您对卷积神经网络有丰富的经验,那么您可能现在正在翻白眼,如果您还没有,那么很快就会有更好的方法,但是不要太快地跳过本章。 我认为扁平化的MNIST是一个非常好的数据集,因为它的外观和行为与我们在许多投入领域(例如,物联网,制造业,生物,制药和医疗用例)中遇到的许多复杂的现实生活问题非常相似)。
类别输出
我们的输出层将为每个类包含一个神经元。 每个类别的关联神经元将经过训练,以将该类别的概率预测为介于 0 和 1 之间的值。我们将使用一种称为 softmax 的特殊激活,以确保所有这些输出总和为 1,我们将介绍 softmax 的详细信息。
这意味着我们将需要为我们的类创建一个二元/分类编码。 例如,如果我们使y = [0, 3, 2, 1]并对其进行分类编码,则将具有如下矩阵y:
幸运的是,Keras 为我们提供了方便的功能来进行这种转换。
成本函数
我们将使用的成本函数称为多项式交叉熵。 多项式交叉熵实际上只是在第 4 章“使用 Keras 进行二分类”中看到的二元交叉熵函数的概括。
让我们一起看看它们,而不只是显示分类交叉熵。 我要断言它们是平等的,然后解释原因:
前面的等式是正确的(m = 2时)
好吧,别害怕。 我知道,这是一堆数学。 绝对交叉熵方程是一直存在于右边的方程。 二元交叉熵紧随其后。 现在,设想m = 2的情况。 在这种情况下,您可能会发现,j = 0和j = 1的y[ij]log(p[ij])的和,对于i中的每个值,等于来自二元交叉熵的结果。 希望这种减少足以使分类交叉熵有意义。 如果没有,我建议选择一些值并进行编码。 只需一秒钟,稍后您将感谢我!
指标
分类交叉熵是一个很好的成本函数,但实际上并不能告诉我们很多我们可以从网络中获得的预测质量。 不幸的是,像 ROC AUC 这样的二分类指标也对我们没有太大帮助,因为我们超越了二分类 AUC 的定义并没有。
鉴于缺少更好的指标,我将使用准确率作为人类可以理解的训练指标。 幸运的是,在这种情况下,我的数据集是平衡的。 正如您所期望的那样,准确率是指真实值与预测值的匹配次数除以数据集的总大小。
训练结束后,我将使用 scikit-learn 的分类报告向我们显示每个类的精确度和召回率。 如果您愿意,也可以为此使用混淆矩阵。
在 Keras 中建立多分类器
由于我们现在有一个定义明确的问题,因此可以开始对其进行编码。 如前所述,这次我们必须对输入和输出进行一些转换。 在我们建立网络的过程中,我将向您展示这些内容。
载入 MNIST
对我们来说幸运的是,在 Keras 中内置了一个 MNIST 加载函数,该函数可以检索 MNIST 数据并为我们加载。 我们需要做的就是导入keras.datasets.mnist并使用load_data()方法,如以下代码所示:
(train_X, train_y), (test_X, test_y) = mnist.load_data()
train_X的形状为50,000 x 28 x 28。正如我们在“模型输入和输出”部分中所述,我们将需要将28x28矩阵展平为 784 个元素向量。 NumPy 使这变得非常容易。 以下代码说明了此技术:
train_X = train_X.reshape(-1, 784)
有了这种方式,我们应该考虑扩展输入。 以前,我们使用 scikit-learn 的StandardScaler。 MNIST 不需要这样做。 由于我们知道每个像素都在 0 到 255 的相同范围内,因此我们可以通过除以255轻松地将值转换为 0 和 1 之间的值,然后在执行操作之前将数据类型显式转换为float32,如以下代码所示:
train_X = train_X.astype('float32')
train_X /= 255
正如我们在“模型输入和输出”部分中所述,在加载数据时,我们可能应该将因变量向量转换为分类向量。 为此,我们将在以下代码的帮助下使用keras.utils.to_categorical():
train_y = to_categorical(train_y)
这样,我们的数据就可以进行训练了!
输入层
我们的输入层实际上与之前的示例保持不变,但我将在此处包括它以使其成为适当的快速参考:
def build_network(input_features=None):
inputs = Input(shape=(input_features,), name="input")
隐藏层
我将使用带有512神经元的第一个隐藏层。 这比输入向量的 784 个元素略小,但这完全不是规则。 同样,此架构只是一个开始,并不一定是最好的。 然后,我将在第二和第三隐藏层中浏览大小,如以下代码所示:
x = Dense(512, activation='relu', name="hidden1")(inputs)
x = Dense(256, activation='relu', name="hidden2")(x)
x = Dense(128, activation='relu', name="hidden3")(x)
输出层
我们的输出层将包含 10 个神经元,一个观察值可能属于其中的每个可能的类。 这对应于我们在y向量上使用to_categorical()时施加的编码:
prediction = Dense(10, activation='softmax', name="output")(x)
如您所见,我们正在使用的激活称为 softmax。 让我们讨论一下softmax是什么,以及为什么有用。
Softmax 激活
想象一下,如果不是使用深层神经网络,而是使用k个逻辑回归,其中每个回归都预测单个类中的成员。 逻辑回归的集合(每个类一个)如下所示:
使用这组逻辑回归的问题是每个逻辑回归的输出都是独立的。 想象一下,在我们的集合中,这些逻辑回归中的一些不确定其所属类别的成员资格,从而导致多个答案在P(Y = k) = 0.5附近。 这使我们无法将这些输出用作k类中类成员资格的总体概率,因为它们不一定总和为 1。
Softmax 压缩所有这些逻辑回归的输出,使它们的总和为 1,从而将其用作整体类成员的概率,从而为我们提供了帮助。
softmax函数如下所示:
(对于j = 1至k类,其中zj / zk是属于k的逻辑回归)
因此,如果将softmax函数放在我们先前的回归集的前面,我们将得到一组类别概率,它们合计为 1,可以用作 k 个类别中成员资格的概率。 这改变了我们的整体函数,如下所示:
先前的函数通常称为多项式逻辑回归。 它有点像一层,仅输出和神经网络。 我们不再频繁使用多项式逻辑回归。 但是,我们当然可以一直使用softmax函数。 对于本书中的大多数多分类问题,我们将使用softmax,因此值得理解。
如果您像我一样,并且发现所有数学知识都难以阅读,那么在代码中查看softmax可能会更容易。 因此,在继续操作之前,请使用以下代码段进行操作:
def softmax(z):
z_exp = [math.exp(x) for x in z]
sum_z_exp = sum(z_exp)
softmax = [round(i / sum_z_exp, 3) for i in z_exp]
return softmax
让我们快速尝试一个例子。 想象一下,我们有一组逻辑输出,如下所示:
z = np.array([0.9, 0.8, 0.2, 0.1, 0.5])
如果应用softmax,我们可以轻松地将这些输出转换为相对的类概率,如下所示:
print(softmax(z))
[0.284, 0.257, 0.141, 0.128, 0.19]
放在一起
现在我们已经涵盖了各个部分,让我们看一下我们的整个网络。 这看起来与我们之前在本书中介绍的模型相似。 但是,我们使用的损失函数categorical_crossentropy在本章的“成本函数”部分中介绍了。
我们将使用以下代码定义网络:
def build_network(input_features=None):
# first we specify an input layer, with a shape == features
inputs = Input(shape=(input_features,), name="input")
x = Dense(512, activation='relu', name="hidden1")(inputs)
x = Dense(256, activation='relu', name="hidden2")(x)
x = Dense(128, activation='relu', name="hidden3")(x)
prediction = Dense(10, activation='softmax', name="output")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=["accuracy"])
return model
训练
现在我们已经定义了神经网络并加载了数据,剩下的就是训练它了。
在本书中以及本书的其他几个示例中,我使用的是称为数据的字典,以绕过train_X,val_X和test_X等各种数据集。 我使用这种表示法来保持代码的可读性,并且因为传递整个字典的必要性经常高于没有。
这是我将如何训练我们刚刚建立的模型的方法。
model = build_network(data["train_X"].shape[1])
model.fit(x=data["train_X"], y=data["train_y"],
batch_size=30,
epochs=50,
validation_data=(data["val_X"], data["val_y"]),
verbose=1,
callbacks=callbacks)
我正在使用与以前相同的回调。 我没有使用我们在第 4 章“使用 Keras 进行二分类”中构建的 ROC AUC 回调,因为 ROC AUC 没有为多分类器明确定义。
存在一些针对该问题的创造性解决方案。 例如,通过成对分析近似多类 ROC 和 ROC 表面下体积都是出色的论文,都可以解决这个问题。 但是,实际上,这些方法及其度量标准很少在 R 中使用,最常在 R 中实现。因此,到目前为止,让我们坚持使用多类准确率,并且远离 R。
让我们观看 TensorBoard 在我们的模型训练中:
在阅读下一段之前,请花点时间思考一下这些图形在告诉我们什么。 得到它了? 好的,让我们继续。
因此,这是一个熟悉的情况。 我们的训练损失正在继续下降,而我们的验证损失正在上升。 我们过拟合。 虽然当然可以选择提前停止,但让我向您展示一些处理过拟合的新技巧。 让我们在下一部分中查看丢弃法和 l2 正则化。 但是,在进行此操作之前,我们应该研究如何使用多类网络来测量准确率和进行预测。
在多类模型中使用 scikit-learn 指标
和以前一样,我们可以借鉴 scikit-learn 的指标来衡量我们的模型。 但是,为此,我们需要从模型的y的分类输出中进行一些简单的转换,因为 scikit-learn 需要使用类标签,而不是二元类指示器。
为了取得飞跃,我们将使用以下代码开始进行预测:
y_softmax = model.predict(data["test_X"])
然后,我们将选择概率最大的类的索引,使用以下代码将其方便地作为该类:
y_hat = y_softmax.argmax(axis=-1)
然后,我们可以像以前一样使用 scikit-learn 的分类报告。 相同的代码如下:
from sklearn.metrics import classification_report
print(classification_report(test_y, y_hat))
现在,我们实际上可以查看所有 10 个类的精度,召回率和 f1 得分。 下图说明了sklearn.metrics.classification_report()的输出:
通过丢弃控制方差
减少深度神经网络过拟合的一种非常好的方法是采用一种称为丢弃法的技术。 丢弃法完全按照其说的去做,它使神经元脱离隐藏层。 运作方式如下。
通过每个小批量,我们将随机选择关闭每个隐藏层中的节点。 想象一下,我们在某个隐藏层中实现了丢弃,并且我们选择了丢弃率为 0.5。 这意味着,对于每个小批量,对于每个神经元,我们都掷硬币以查看是否使用该神经元。 这样,您可能会随机关闭该隐藏层中大约一半的神经元:
如果我们一遍又一遍地执行此操作,就好像我们正在训练许多较小的网络。 模型权重保持相对较小,每个较小的网络不太可能过拟合数据。 这也迫使每个神经元减少对其他神经元的依赖。
丢弃法效果惊人,可以很好地解决您可能遇到的许多(如果不是大多数)深度学习问题的过拟合问题。 如果您具有高方差模型,则丢弃是减少过拟合的好选择。
Keras 包含一个内置的Dropout层,我们可以轻松地在网络中使用它来实现Dropout。 Dropout层将简单地随机关闭前一层神经元的输出,以使我们轻松地改造网络以使用Dropout。 要使用它,除了我们正在使用的其他层类型之外,我们还需要首先导入新层,如以下代码所示:
from keras.layers import Input, Dense, Dropout
然后,我们只需将Dropout层插入模型,如以下代码所示:
def build_network(input_features=None):
# first we specify an input layer, with a shape == features
inputs = Input(shape=(input_features,), name="input")
x = Dense(512, activation='relu', name="hidden1")(inputs)
x = Dropout(0.5)(x)
x = Dense(256, activation='relu', name="hidden2")(x)
x = Dropout(0.5)(x)
x = Dense(128, activation='relu', name="hidden3")(x)
x = Dropout(0.5)(x)
prediction = Dense(10, activation='softmax', name="output")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='categorical_crossentropy',
metrics=["accuracy"])
return model
这是我们先前使用的确切模型; 但是,我们在每个Dense层之后都插入了Dropout层,这是我通常在实现丢弃时开始的方式。 像其他模型架构决策一样,您可以选择仅在某些层,所有层或没有层中实现丢弃。 您还可以选择更改退出/保留概率; 但是,我确实建议从 0.5 开始,因为它通常效果很好。
一个安全的选择是在每一层都退出,保持概率为 0.5。 不错的第二种尝试是仅在第一层使用丢弃。
让我们用丢弃法训练我们的新模型,看看它与我们的第一次尝试相比如何:
首先让我们看一下验证准确率。 使用丢弃模型的训练速度与未规范模型的训练速度一样快,但是在这种情况下,它的确似乎很快就开始加速。 看看在第 44 个周期的验证准确率。它比非正规模型略好。
现在,让我们看看验证损失。 您可以看到丢弃法对模型过拟合的影响,而且确实非常明显。 虽然仅转换为最终产品的少量改进,但丢弃法表现相当不错,可以防止我们的验证损失提升。
通过正则化控制方差
正则化是控制过拟合的另一种方法,当模型中的各个权重增大时会对其进行惩罚。 如果您熟悉线性模型(例如线性和逻辑回归),那么它与在神经元级别应用的技术完全相同。 可以使用两种形式的正则化,称为 L1 和 L2,来对神经网络进行正则化。 但是,由于 L2 正则化计算效率更高,因此几乎总是在神经网络中使用它。
快速地,我们需要首先规范化成本函数。 如果我们将C[0],分类交叉熵作为原始成本函数,则正规化的cost函数将如下所示:
这里,λ是可以增加或减少以更改应用的正则化量的正则化参数。 此正则化参数会惩罚较大的权重值,从而使网络总体上希望具有较小的权重。
要更深入地了解神经网络中的正则化,请查看 Michael Nielsen 的《神经网络和深度学习》的第 3 章。
可以将正则化应用于 Keras 层中的权重,偏差和激活。 我将使用带有默认参数的 L2 演示此技术。 在以下示例中,我将正则化应用于每个隐藏层:
def build_network(input_features=None):
# first we specify an input layer, with a shape == features
inputs = Input(shape=(input_features,), name="input")
x = Dense(512, activation='relu', name="hidden1", kernel_regularizer='l2') \
(inputs)
x = Dense(256, activation='relu', name="hidden2", kernel_regularizer='l2')(x)
x = Dense(128, activation='relu', name="hidden3", kernel_regularizer='l2')(x)
prediction = Dense(10, activation='softmax', name="output")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer='adam', loss='categorical_crossentropy',
metrics=["accuracy"])
return model
因此,让我们将默认的 L2 正则化与其他两个模型进行比较。 下图显示了比较:
不幸的是,我们的新 L2 正则化网络很容易找到。 在这种情况下,似乎 L2 正则化效果很好。 我们的网络现在偏差严重,对其他两个方面的了解还不够。
如果我真的确定要使用正则化来解决此问题,那么我将首先更改正则化率并尝试找到更合适的值,但我们相距甚远,我对此表示怀疑,我们会做得比我们更好 dropout模型。
总结
在本章中,我们实际上已经开始了解深度神经网络在进行多分类时的威力。 我们详细介绍了softmax函数,然后我们构建并训练了一个网络来将手写数字分为 10 个各自的类别。
最后,当我们注意到模型过拟合时,我们尝试同时使用丢弃和 L2 正则化来减少模型的方差。
到目前为止,您已经看到深度神经网络需要很多选择,关于架构的选择,学习率,甚至是正则化率。 我们将在下一章中学习如何优化这些选择。
六、超参数优化
使用深度神经网络的最大缺点之一是它们具有许多应优化的超参数,以使网络发挥最佳表现。 在前面的每个章节中,我们都遇到但没有涵盖超参数估计的挑战。 超参数优化是一个非常重要的话题。 在大多数情况下,这是一个未解决的问题,尽管我们不能涵盖本书的全部主题,但我认为它仍然值得一章。
在本章中,我将为您提供一些我认为是选择超参数的实用建议。 可以肯定的是,由于本章是基于我自己的经验,因此本章可能会有些偏颇和偏颇。 我希望经验会有所帮助,同时也带您进一步对该主题进行调查。
我们将在本章介绍以下主题:
- 是否应该将网络架构视为超参数?
- 我们应该优化哪些超参数?
- 超参数优化策略
是否应该将网络架构视为超参数?
在构建最简单的网络时,我们必须对网络架构做出各种选择。 我们应该使用 1 个隐藏层还是 1,000 个? 每层应包含多少个神经元? 他们都应该使用relu激活函数还是tanh? 我们应该在每个隐藏层上还是仅在第一层上使用丢弃? 在设计网络架构时,我们必须做出许多选择。
在最典型的情况下,我们穷举搜索每个超参数的最佳值。 但是,要穷举搜索网络架构并不容易。 实际上,我们可能没有时间或计算能力。 我们很少看到研究人员通过穷举搜索来寻找最佳架构,因为选择的数量非常多,而且存在不只一个正确的答案。 取而代之的是,我们看到该领域的研究人员通过实验尝试建立已知的架构,以尝试创建新的新颖架构并改善现有架构。
因此,在介绍详尽搜索超参数的策略之前,让我们看一下两种推论出合理的,甚至不是最佳的网络架构的策略。
找到一个巨人然后站在他的肩膀上
沙特尔的伯纳德(Bernard of Chartres)被赋予了通过借鉴他人的发现来学习的概念。 但是,正是艾萨克·牛顿(Isaac Newton)说:“如果我进一步观察,那就是站在巨人的肩膀上。” 要明确的是,这正是我在这里建议的。
如果我要设计一个用于新的深度学习问题的网络架构,我要做的第一件事就是尝试找到一个令人满意的方式,以前已经解决了类似的问题。 尽管可能没有人能够解决您面临的任务,但可能存在类似的情况。
很可能存在几种可能的解决方案。 如果是这样,并且在时间允许的情况下,每次运行几次的平均结果可能会告诉您哪个运行效果最好。 当然,在这里我们发现自己很快进入了研究领域。
添加,直到过拟合,然后进行正则化
希望通过寻找类似问题的架构,您至少接近适合您的架构。 您如何做才能进一步优化网络架构?
- 在多个实验运行中,添加层和/或神经元,直到您的网络开始针对问题过拟合。 在深度学习中,添加单元,直到您不再具有高偏差模型为止。
- 一旦开始过拟合,您就会发现一些网络架构能够很好地拟合训练数据,甚至可能拟合得很好。 在这一点上,您应该集中精力通过使用丢弃,正则化,提早停止等方法来减少方差。
这种方法通常归因于著名的神经网络研究员 Geoffrey Hinton。 这是一个有趣的想法,因为它使过拟合不是要避免的事情,而是构建网络架构的良好第一步。
尽管没有规则可供我们选择最佳网络架构,并且可能存在许多最佳架构,但我发现这种策略在实践中对我来说非常有效。
实用建议
如果您对上述内容不太了解,我同意。 这对我也不是,我也不希望那样。 您当然可以在一组预定义的配置之间搜索最佳的网络架构,这也是正确的方法。 实际上,它可以说是更正确,更严格。 此过程旨在为您提供实用的建议,以帮助您在尽可能短的时间内达到最佳状态。
我们应该优化哪些超参数?
即使您遵循我的建议并选择了一个足够好的架构,您也可以并且仍然应该尝试在该架构中搜索理想的超参数。 我们可能要搜索的一些超参数包括:
- 我们选择的优化器。 到目前为止,我一直在使用 Adam,但是 rmsprop 优化器或调整良好的 SGD 可能会更好。
- 每个优化器都有一组我们可能需要调整的超参数,例如学习率,动量和衰减。
- 网络权重初始化。
- 神经元激活。
- 正则化参数(例如丢弃概率)或 12 正则化中使用的正则化参数。
- 批次大小。
如上所述,这不是详尽的清单。 当然,您可以尝试更多的选择,包括在每个隐藏层中引入可变数量的神经元,每层中丢弃概率的变化等等。 就像我们一直暗示的那样,超参数的可能组合是无限的。 这些选择也很可能并非独立于网络架构,添加和删除层可能会为这些超参数中的任何一个带来新的最佳选择。
超参数优化策略
在本章的这一点上,我们建议,在大多数情况下,尝试我们可能想尝试的每个超参数组合在计算上都是不可能的,或者至少是不切实际的。 深度神经网络肯定会花费很长时间进行训练。 尽管您可以并行处理问题并投入计算资源,但搜索超参数的最大限制可能仍然是时间。
如果时间是我们最大的限制,并且我们无法合理地探索拥有的所有可能性,那么我们将必须制定一种策略,使我们在拥有的时间内获得最大的效用。
在本节的其余部分,我将介绍一些用于超参数优化的常用策略,然后向您展示如何使用我最喜欢的两种方法在 Keras 中优化超参数。
通用策略
在所有机器学习模型中都有一套通用的超参数优化策略。 从总体上讲,这些策略包括:
- 网格搜索
- 随机搜索
- 贝叶斯优化
- 遗传算法
- 机器学习的超参数
网格搜索只是尝试尝试所有事物,或者至少尝试离散事物,然后报告我们用蛮力找到的最佳超参数的最佳组合。 可以保证在我们确定的参数空间中找到最佳解决方案,以及其他较差的解决方案。
网格搜索对于深度学习并不是很实用。 除了最基本的深度神经网络,我们无法现实地探索所有可能参数的每个可能值。 使用随机搜索,我们从每个参数分布中随机抽样,并尝试其中的n,其中(n x每个示例训练时间)是我们愿意分配给这个问题的时间预算。
贝叶斯优化方法使用以前的观察结果来预测接下来要采样的超参数集。 尽管贝叶斯优化方法通常胜过蛮力技术,但目前的研究表明,与穷举方法相比,表现提升较小。 此外,由于贝叶斯方法取决于先前的经验,因此无论如何都不会令人尴尬地并行进行。
遗传算法是机器学习中非常有趣且活跃的研究领域。 但是,我目前的观点是,它们也不是深度神经网络参数优化的理想选择,因为它们再次依赖于先前的经验。
该领域中的一些最新研究着眼于训练神经网络,该神经网络可以预测给定网络架构的最佳参数。 可以参数化模型的模型的想法当然非常有趣,这是一个值得密切关注的地方。 这也可能是我们获得天网的方式。 只有时间证明一切。
在 scikit-learn 中使用随机搜索
使用 scikit-learn 可以轻松实现网格搜索和随机搜索。 在此示例中,我们将使用 Keras 的KerasClassifier类包装模型并使其与 scikit-learn API 兼容。 然后,我们将使用 scikit-learn 的RandomSearchCV类进行超参数搜索。
为此,我们将从稍微更改现在熟悉的模型构建函数开始。 我们将使用我们要搜索的超参数对其进行参数化,如以下代码所示:
def build_network(keep_prob=0.5, optimizer='adam'):
inputs = Input(shape=(784,), name="input")
x = Dense(512, activation='relu', name="hidden1")(inputs)
x = Dropout(keep_prob)(x)
x = Dense(256, activation='relu', name="hidden2")(x)
x = Dropout(keep_prob)(x)
x = Dense(128, activation='relu', name="hidden3")(x)
x = Dropout(keep_prob)(x)
prediction = Dense(10, activation='softmax', name="output")(x)
model = Model(inputs=inputs, outputs=prediction)
model.compile(optimizer=optimizer, loss='categorical_crossentropy',
metrics=["accuracy"])
return model
在此示例中,我想搜索一个理想的丢弃值,并且我想尝试几个不同的优化器。 为了实现这一点,我需要将它们作为参数包含在函数中,以便可以通过我们的随机搜索方法对其进行更改。 当然,我们可以使用相同的方法来参数化和测试许多其他网络架构选择,但是我们在这里保持简单。
接下来,我们将创建一个函数,该函数返回一个字典,其中包含我们想搜索的所有可能的超参数及其值空间,如以下代码所示:
def create_hyperparameters():
batches = [10, 20, 30, 40, 50]
optimizers = ['rmsprop', 'adam', 'adadelta']
dropout = np.linspace(0.1, 0.5, 5)
return {"batch_size": batches, "optimizer": optimizers,
"keep_prob": dropout}
剩下的就是使用RandomSearchCV将这两部分连接在一起。 首先,我们将模型包装到keras.wrappers.scikit_learn.KerasClassifier中,以便与 scikit-learn 兼容,如以下代码所示:
model = KerasClassifier(build_fn=build_network, verbose=0)
接下来,我们将使用以下代码获得超参数字典:
hyperparameters = create_hyperparameters()
然后,最后,我们将创建一个RandomSearchCV对象,该对象将用于搜索模型的参数空间,如以下代码所示:
search = RandomizedSearchCV(estimator=model, param_distributions=hyperparameters, n_iter=10, n_jobs=1, cv=3, verbose=1)
拟合此RandomizedSearchCV对象后,它将从参数分布中随机选择值并将其应用于模型。 它将执行 10 次(n_iter=10),并且将尝试每种组合 3 次,因为我们使用了 3 倍交叉验证。 这意味着我们将总共拟合模型 30 次。 使用每次运行的平均准确率,它将返回最佳模型作为类属性.best_estimator,并且将返回最佳参数作为.best_params_。
为了适合它,我们只需调用它的fit方法,就好像它是一个模型一样,如以下代码所示:
search.fit(data["train_X"], data["train_y"])
print(search.best_params_)
在 Tesla K80 GPU 实例上,在上述网格上拟合第 5 章,“使用 Keras 进行多分类”所使用的 MNIST 模型。 在完成本节之前,让我们看一下搜索的一些输出,如以下代码所示:
Using TensorFlow backend.
Fitting 3 folds for each of 10 candidates, totalling 30 fits
tensorflow/core/common_runtime/gpu/gpu_device.cc:1030] Found device 0 with properties:
name: Tesla K80 major: 3 minor: 7 memoryClockRate(GHz): 0.8235
pciBusID: 0000:00:1e.0
totalMemory: 11.17GiB freeMemory: 11.10GiB
tensorflow/core/common_runtime/gpu/gpu_device.cc:1120] Creating TensorFlow device (/device:GPU:0) -> (device: 0, name: Tesla K80, pci bus id: 0000:00:1e.0, compute capability: 3.7)
[Parallel(n_jobs=1)]: Done 30 out of 30 | elapsed: 8.8min finished
{'keep_prob': 0.20000000000000001, 'batch_size': 40, 'optimizer': 'adam'}
如您在此输出中看到的,在 10 次运行中,加粗的超参数似乎是表现最好的集合。 当然,我们当然可以运行更多的迭代,并且我们可能会找到一个更好的选择。 我们的预算仅由时间,耐心以及云帐户附带的信用卡决定。
Hyperband
Hyperband 是一项超参数优化技术,由 Lisha Li,Kevin Jamieson,Guilia DeSalvo,Afshin Rostamizadeh 和 Ameet Talwalker 于 2016 年在伯克利开发。 您可以在这里阅读他们的原始论文。
想象一下,就像我们在RandomSearchCV中所做的那样,随机采样许多潜在的超参数集。 完成RandomSearchCV后,它将选择一个单一的超参数配置作为其采样的最优值。 Hyperband 利用这样的思想,即即使经过少量迭代,最佳的超参数配置也可能会胜过其他配置。 Hyperband 中的乐队来自土匪,指的是基于多臂土匪技术(用于优化竞争选择之间的资源分配以优化表现为目标的技术)的勘探与开发。
使用 Hyperband,我们可以尝试一些可能的配置集(n),仅训练一次迭代。 作者将迭代一词留作多种可能的用途。 但是,我将周期作为迭代。 一旦完成第一个训练循环,就将根据表现对结果进行配置。 然后,对该列表的上半部分进行大量迭代的训练。 然后重复进行减半和剔除的过程,我们得到了一些非常小的配置集,我们将针对在搜索中定义的完整迭代次数进行训练。 与在每种可能的配置中搜索最大周期相比,此过程使我们在更短的时间内获得了最佳超参数集。
在本章的 GitHub 存储库中,我在hyperband.py中包括了hyperband算法的实现。 此实现主要源自 FastML 的实现,您可以在这个页面中找到。 要使用它,您需要首先实例化一个hyperband对象,如以下代码所示:
from hyperband import Hyperband
hb = Hyperband(data, get_params, try_params)
Hyperband 构造器需要三个参数:
data:到目前为止,我在示例中一直在使用的数据字典get_params:用于从我们正在搜索的超参数空间中采样的函数的名称try_param:可用于评估n_iter迭代的超参数配置并返回损失的函数的名称
在下面的示例中,我实现了get_params以在参数空间中以统一的方式进行采样:
def get_params():
batches = np.random.choice([5, 10, 100])
optimizers = np.random.choice(['rmsprop', 'adam', 'adadelta'])
dropout = np.random.choice(np.linspace(0.1, 0.5, 10))
return {"batch_size": batches, "optimizer": optimizers,
"keep_prob": dropout}
如您所见,所选的超参数配置将作为字典返回。
接下来,可以实现try_params以在超参数配置上针对指定的迭代次数拟合模型,如下所示:
def try_params(data, num_iters, hyperparameters):
model = build_network(keep_prob=hyperparameters["keep_prob"],
optimizer=hyperparameters["optimizer"])
model.fit(x=data["train_X"], y=data["train_y"],
batch_size=hyperparameters["batch_size"],
epochs=int(num_iters))
loss = model.evaluate(x=data["val_X"], y=data["val_y"], verbose=0)
return {"loss": loss}
try_params函数返回一个字典,可用于跟踪任何数量的度量; 但是,由于它用于比较运行,因此需要损失。
通过在对象上调用.run()方法,hyperband对象将通过我们上面描述的算法运行。
results = hb.run()
在这种情况下,results将是每次运行,其运行时间和测试的超参数的字典。 因为即使这种高度优化的搜索都需要花费大量时间,并且 GPU 时间也很昂贵,所以我将 MNIST 搜索的结果包括在本章的 GitHub 存储库的hyperband-output-mnist.txt中,可以在以下位置找到。
总结
超参数优化是从我们的深度神经网络获得最佳效果的重要一步。 寻找搜索超参数的最佳方法是机器学习研究的一个开放而活跃的领域。 尽管您当然可以将最新技术应用于自己的深度学习问题,但您需要在决策中权衡实现的复杂性和搜索运行时间。
有一些与网络架构有关的决策可以肯定地进行详尽地搜索,但是,如我上面提供的那样,一组启发式方法和最佳实践可能使您足够接近甚至减少搜索参数的数量。
最终,超参数搜索是一个经济问题,任何超参数搜索的第一部分都应考虑您的计算时间和个人时间预算,以试图找出最佳的超参数配置。
本章总结了深度学习的基础。 在下一章中,我们将从计算机视觉入手,介绍神经网络的一些更有趣和更高级的应用。
七、从头开始训练 CNN
深度神经网络彻底改变了计算机视觉。 实际上,我认为在最近几年中计算机视觉的进步已经使深层神经网络成为许多消费者每天使用的东西。 我们已经在第 5 章“使用 Keras 进行多分类”中使用计算机视觉分类器,其中我们使用了深度网络对手写数字进行分类。 现在,我想向您展示卷积层如何工作,如何使用它们以及如何在 Keras 中构建自己的卷积神经网络以构建更好,功能更强大的深度神经网络来解决计算机视觉问题。
我们将在本章介绍以下主题:
- 卷积介绍
- 在 Keras 中训练卷积神经网络
- 使用数据增强
卷积介绍
经过训练的卷积层由称为过滤器的许多特征检测器组成,这些特征检测器在输入图像上滑动作为移动窗口。 稍后我们将讨论过滤器内部的内容,但现在它可能是一个黑匣子。 想象一个已经训练过的过滤器。 也许该过滤器已经过训练,可以检测图像中的边缘,您可能会认为这是黑暗与明亮之间的过渡。 当它经过图像时,其输出表示它检测到的特征的存在和位置,这对于第二层过滤器很有用。 稍微扩展一下我们的思想实验,现在想象第二个卷积层中的一个过滤器,它也已经被训练过了。 也许这个新层已经学会了检测直角,其中存在由上一层找到的两个边缘。 不断地我们去; 随着我们添加层,可以了解更多复杂的特征。 特征层次结构的概念对于卷积神经网络至关重要。 下图来自 Honglak Lee 等人的《使用卷积深度信念网络的无监督学习层次表示》[2011],非常好地说明了特征层次结构的概念:
这是一种非常强大的技术,它比我们先前在 MNIST 上使用的深度学习flatten和classify方法具有多个优势。 我们将在短期内讨论这些内容,但首先让我们深入了解过滤器。
卷积层如何工作?
在上一节中,我说过卷积层是一组充当特征检测器的过滤器。 在我们深入探讨该架构之前,让我们回顾一下卷积实际上是什么的数学。
让我们首先手动将以下4 x 4矩阵与3 x 3矩阵卷积,我们将其称为过滤器。 卷积过程的第一步是获取过滤器与4 x 4矩阵的前九个框的按元素乘积:
完成此操作后,我们将过滤器滑到一行上并执行相同的操作。 最后,我们将过滤器向下滑动,然后再次滑动。 卷积过程一旦完成,将使我们剩下2x2矩阵,如下图所示:
从技术上讲,这不是卷积,而是互相关。 按照惯例,我们将其称为卷积,并且就我们的目的而言,差异确实很小。
三维卷积
MNIST 是一个灰度示例,我们可以将每个图像表示为二维矩阵中从 0 到 255 的像素强度值。 但是,大多数时候,我们将使用彩色图像。 彩色图像实际上是三维矩阵,其中维是图像高度,图像宽度和颜色。 这将为图像中的每个像素生成一个矩阵,分别具有红色,蓝色和绿色值。
虽然我们先前展示的是二维过滤器,但我们可以通过在(高度,宽度,3(颜色))矩阵与3 x 3 x 3之间进行卷积来将其思想轻松转换为三个维度。 过滤。 最后,当我们在矩阵的所有三个轴上进行逐元素乘积运算时,仍然剩下二维输出。 提醒一下,这些高维矩阵通常称为张量,而我们正在做的就是使它们流动。
卷积层
之前我们已经讨论了由多个线性函数单元以及一些非线性(例如relu)组成的深度神经网络层。 在卷积层中,每个单元都是一个过滤器,结合了非线性。 例如,可以在 Keras 中定义卷积层,如下所示:
from keras.layers import Conv2D
Conv2D(64, kernel_size=(3,3), activation="relu", name="conv_1")
在此层中,有 64 个独立的单元,每个单元都有3 x 3 x 3过滤器。 卷积操作完成后,每个单元都会像传统的完全连接层中那样为输出添加偏置和非线性(稍后会详细介绍该术语)。
在继续之前,让我们快速浏览一下示例的维度,以便确保我们都在同一页面上。 想象一下,我们有一个32 x 32 x 3的输入图像。 现在,我们将其与上述卷积层进行卷积。 该层包含 64 个过滤器,因此输出为30 x 30 x 64。 每个过滤器输出一个30 x 30矩阵。
卷积层的好处
因此,现在您希望对卷积层的工作原理有所了解,让我们讨论为什么我们要进行所有这些疯狂的数学运算。 为什么我们要使用卷积层而不是以前使用的普通层?
假设我们确实使用了普通层,以得到与之前讨论的相同的输出形状。 我们从32 x 32 x 3图像开始,所以总共有 3,072 个值。 我们剩下一个30 x 30 x 64矩阵。 总共有 57,600 个值。 如果我们要使用完全连接的层来连接这两个矩阵,则该层将具有 176,947,200 个可训练参数。 那是 1.76 亿。
但是,当我们使用上面的卷积层时,我们使用了 64 个3 x 3 x 3过滤器,这将导致 1,728 个可学习权重加 64 个偏差(总共 1,792 个参数)。
因此,显然卷积层需要的参数要少得多,但是为什么这很重要呢?
参数共享
由于过滤器是在整个图像中使用的,因此过滤器会学会检测特征,而不管其在图像中的位置如何。 事实证明,这非常有用,因为它为我们提供了平移不变性,这意味着我们可以检测到重要的内容,而不管其在整个图像中的朝向。
回想一下 MNIST,不难想象我们可能想检测 9 的循环,而不管它在照片中的位置如何。 提前思考,想象一个将图片分类为猫或汽车的分类器。 容易想象有一组过滤器可以检测出像汽车轮胎一样复杂的东西。 无论轮胎的方向在图像中的什么位置,检测该轮胎都是有用的,因为轮胎之类的东西强烈表明该图像不是猫(除非图像是驾驶汽车的猫)。
本地连接
过滤器由于其固定大小而着重于相邻像素之间的连通性。 这意味着他们将最强烈地学习本地特征。 当与其他过滤器以及层和非线性结合使用时,这使我们逐渐关注更大,更复杂的特征。 确实需要这种局部化特征的堆叠,这也是卷积层如此之大的关键原因。
池化层
除了卷积层,卷积神经网络通常使用另一种类型的层,称为池化层。 当添加卷积层时,使用池化层来减少卷积网络的维数,这会减少过拟合。 它们具有使特征检测器更坚固的附加好处。
池化层将矩阵划分为非重叠部分,然后通常在每个区域中采用最大值(在最大池化的情况下)。 可替代地,可以采用平均值。 但是,目前很少使用。 下图说明了此技术:
如我们所料,池化层在 Keras 中很容易实现。 以下代码可用于池化各层:
from keras.layers import MaxPooling2D
pool1 = MaxPooling2D(pool_size=(2, 2), name="pool_1")
在这里,我们将池窗口定义为2 x 2。
尽管我们之前没有讨论过填充,但是在某些架构中,通常将卷积层或池化层的输入填充为 0,以使输出尺寸等于输入。 Keras 的卷积层和池化层中的默认值都是有效填充,这意味着按惯例没有填充。 如果需要,参数padding="same"将应用填充。
批量标准化
批量规范化有助于我们的网络整体表现更好,学习速度更快。 批量规范化在应用中也很容易理解。 但是,为什么它起作用,仍然受到研究人员的争议。
使用批量归一化时,对于每个小批量,我们可以在每个非线性之后(或之前)对那个批量进行归一化,使其平均值为 0,单位方差。 这使每一层都可以从中学习标准化输入,从而使该层的学习效率更高。
批归一化层很容易在 Keras 中实现,本章的示例将在每个卷积层之后使用它们。 以下代码用于批量规范化:
from keras.layers import BatchNormalization
x = BatchNormalization(name="batch_norm_1")
在 Keras 中训练卷积神经网络
现在我们已经介绍了卷积神经网络的基础知识,是时候构建一个了。 在本案例研究中,我们将面对一个众所周知的问题,即 CIFAR-10。 该数据集由 Alex Krizhevsky,Vinod Nair 和 Geoffrey Hinton 创建。
输入
CIFAR-10 数据集由属于 10 类的 60,000 张32 x 32彩色图像组成,每类 6,000 张图像。 我将使用 50,000 张图像作为训练集,使用 5,000 张图像作为验证集,并使用 5,000 张图像作为测试集。
卷积神经网络的输入张量层将为(N, 32, 32, 3),我们将像以前一样将其传递给build_network函数。 以下代码用于构建网络:
def build_network(num_gpu=1, input_shape=None):
inputs = Input(shape=input_shape, name="input")
输出
该模型的输出将是 0-9 之间的类别预测。 我们将使用与 MNIST 相同的 10 节点softmax。 令人惊讶的是,我们的输出层没有任何变化。 我们将使用以下代码来定义输出:
output = Dense(10, activation="softmax", name="softmax")(d2)
成本函数和指标
在第 5 章中,我们使用分类交叉熵作为多分类器的损失函数。 这只是另一个多分类器,我们可以继续使用分类交叉熵作为我们的损失函数,并使用准确率作为度量。 我们已经开始使用图像作为输入,但是幸运的是我们的成本函数和指标保持不变。
卷积层
如果您开始怀疑此实现中是否会有任何不同之处,那就是这里。 我将使用两个卷积层,分别进行批量规范化和最大池化。 这将要求我们做出很多选择,当然我们以后可以选择作为超参数进行搜索。 不过,最好先让某些东西开始工作。 正如 Donald Knuth 所说,过早的优化是万恶之源。 我们将使用以下代码片段定义两个卷积块:
# convolutional block 1
conv1 = Conv2D(64, kernel_size=(3,3), activation="relu", name="conv_1")(inputs)
batch1 = BatchNormalization(name="batch_norm_1")(conv1)
pool1 = MaxPooling2D(pool_size=(2, 2), name="pool_1")(batch1)
# convolutional block 2
conv2 = Conv2D(32, kernel_size=(3,3), activation="relu", name="conv_2")(pool1)
batch2 = BatchNormalization(name="batch_norm_2")(conv2)
pool2 = MaxPooling2D(pool_size=(2, 2), name="pool_2")(batch2)
因此,很明显,我们在这里有两个卷积块,它们由一个卷积层,一个批量规范化层和一个池化层组成。
在第一块中,我使用具有relu激活函数的 64 个3 x 3过滤器。 我使用的是有效(无)填充,跨度为 1。批量规范化不需要任何参数,并且实际上不是可训练的。 池化层使用2 x 2池化窗口,有效填充和跨度为 2(窗口尺寸)。
第二个块几乎相同。 但是,我将过滤器数量减半为 32。
尽管在该架构中有许多旋钮可以转动,但我首先要调整的是卷积的内核大小。 内核大小往往是一个重要的选择。 实际上,一些现代的神经网络架构(例如 Google 的 Inception)使我们可以在同一卷积层中使用多个过滤器大小。
全连接层
经过两轮卷积和合并后,我们的张量变得相对较小和较深。 在pool_2之后,输出尺寸为(n, 6, 6, 32)。
我们希望在这些卷积层中提取此6 x 6 x 32张量表示的相关图像特征。 为了使用这些特征对图像进行分类,在进入最终输出层之前,我们将将该张量连接到几个完全连接的层。
在此示例中,我将使用 512 神经元完全连接层,256 神经元完全连接层以及最后的 10 神经元输出层。 我还将使用丢弃法来帮助防止过拟合,但只有一点点! 该过程的代码如下,供您参考:
from keras.layers import Flatten, Dense, Dropout
# fully connected layers
flatten = Flatten()(pool2)
fc1 = Dense(512, activation="relu", name="fc1")(flatten)
d1 = Dropout(rate=0.2, name="dropout1")(fc1)
fc2 = Dense(256, activation="relu", name="fc2")(d1)
d2 = Dropout(rate=0.2, name="dropout2")(fc2)
我之前没有提到上面的flatten层。 flatten层完全按照其名称的含义执行。 将flattens,n x 6 x 6 x 32张量flattens转换为n x 1152向量。 这将作为全连接层的输入。
Keras 中的多 GPU 模型
许多云计算平台可以提供包含多个 GPU 的实例。 随着我们模型的规模和复杂性的增长,您可能希望能够跨多个 GPU 并行化工作负载。 这在本机 TensorFlow 中可能涉及到一些过程,但是在 Keras 中,这只是一个函数调用。
正常构建模型,如以下代码所示:
model = Model(inputs=inputs, outputs=output)
然后,我们借助以下代码将该模型传递给keras.utils.multi_gpu_model:
model = multi_gpu_model(model, num_gpu)
在此示例中,num_gpu是我们要使用的 GPU 的数量。
训练
将模型放在一起,并结合我们新的 CUDA GPU 功能,我们提出了以下架构:
def build_network(num_gpu=1, input_shape=None):
inputs = Input(shape=input_shape, name="input")
# convolutional block 1
conv1 = Conv2D(64, kernel_size=(3,3), activation="relu",
name="conv_1")(inputs)
batch1 = BatchNormalization(name="batch_norm_1")(conv1)
pool1 = MaxPooling2D(pool_size=(2, 2), name="pool_1")(batch1)
# convolutional block 2
conv2 = Conv2D(32, kernel_size=(3,3), activation="relu",
name="conv_2")(pool1)
batch2 = BatchNormalization(name="batch_norm_2")(conv2)
pool2 = MaxPooling2D(pool_size=(2, 2), name="pool_2")(batch2)
# fully connected layers
flatten = Flatten()(pool2)
fc1 = Dense(512, activation="relu", name="fc1")(flatten)
d1 = Dropout(rate=0.2, name="dropout1")(fc1)
fc2 = Dense(256, activation="relu", name="fc2")(d1)
d2 = Dropout(rate=0.2, name="dropout2")(fc2)
# output layer
output = Dense(10, activation="softmax", name="softmax")(d2)
# finalize and compile
model = Model(inputs=inputs, outputs=output)
if num_gpu > 1:
model = multi_gpu_model(model, num_gpu)
model.compile(optimizer='adam', loss='categorical_crossentropy',
metrics=["accuracy"])
return model
我们可以使用它来构建我们的模型:
model = build_network(num_gpu=1, input_shape=(IMG_HEIGHT, IMG_WIDTH, CHANNELS))
然后,我们可以满足您的期望:
model.fit(x=data["train_X"], y=data["train_y"],
batch_size=32,
epochs=200,
validation_data=(data["val_X"], data["val_y"]),
verbose=1,
callbacks=callbacks)
在我们训练该模型时,您会注意到过拟合是一个紧迫的问题。 即使只有相对较小的两个卷积层,我们也已经有点过拟合了。
您可以从以下图形中看到过拟合的影响:
不足为奇,50,000 次观察不是很多数据,尤其是对于计算机视觉问题。 在实践中,计算机视觉问题得益于非常大的数据集。 实际上,Chen Sun 指出,附加数据倾向于以数据量的对数线性帮助计算机视觉模型。 不幸的是,在这种情况下,我们无法真正找到更多数据。 但是也许我们可以做些。 接下来让我们讨论数据增强。
使用数据增强
数据增强是一种将变换应用于图像并使用原始图像和变换后的图像进行训练的技术。 想象一下,我们有一个训练类,里面有一只猫:
如果将水平翻转应用于此图像,我们将得到如下所示的内容:
当然,这是完全相同的图像,但是我们可以将原始图像和转换图像用作训练示例。 这不像我们训练中的两只猫那么好。 但是,它的确使我们可以告诉计算机,无论猫面对什么方向,猫都是猫。
在实践中,我们可以做的不仅仅是水平翻转。 当有意义时,我们也可以垂直翻转,移动和随机旋转图像。 这使我们能够人为地放大我们的数据集,并使它看起来比实际的更大。 当然,您只能将其推到目前为止,但这是在存在少量数据的情况下防止过拟合的一个非常强大的工具。
Keras ImageDataGenerator
不久前,进行图像增强的唯一方法是对转换进行编码,并将其随机应用于训练集,然后将转换后的图像保存在磁盘上(上下坡,在雪中)。 对我们来说幸运的是,Keras 现在提供了ImageDataGenerator类,可以在我们训练时即时应用转换,而无需手工编码转换。
我们可以通过实例化ImageDataGenerator来创建一个数据生成器对象,如下所示:
def create_datagen(train_X):
data_generator = ImageDataGenerator(
rotation_range=20,
width_shift_range=0.02,
height_shift_range=0.02,
horizontal_flip=True)
data_generator.fit(train_X)
return data_generator
在此示例中,我同时使用了移位,旋转和水平翻转。 我只使用很小的移位。 通过实验,我发现更大的变化太多了,而且我的网络实际上无法学到任何东西。 您的经验会随着您的问题而变化,但是我希望较大的图像更能容忍移动。 在这种情况下,我们使用 32 个像素的图像,这些图像非常小。
用生成器训练
如果您以前没有使用过生成器,则它就像迭代器一样工作。 每次调用ImageDataGenerator .flow()方法时,它都会产生一个新的训练小批量,并将随机变换应用于所馈送的图像。
Keras Model类带有.fit_generator()方法,该方法使我们可以使用生成器而不是给定的数据集:
model.fit_generator(data_generator.flow(data["train_X"], data["train_y"], batch_size=32),
steps_per_epoch=len(data["train_X"]) // 32,
epochs=200,
validation_data=(data["val_X"], data["val_y"]),
verbose=1,
callbacks=callbacks)
在这里,我们用生成器替换了传统的x和y参数。 最重要的是,请注意steps_per_epoch参数。 您可以从训练集中任意采样替换次数,并且每次都可以应用随机变换。 这意味着我们每个周期可以使用的迷你批数比数据还多。 在这里,我将仅根据观察得到的样本数量进行采样,但这不是必需的。 如果可以,我们可以并且应该将这个数字提高。
在总结之前,让我们看一下这种情况下图像增强的好处:
如您所见,仅一点点图像增强确实帮助了我们。 不仅我们的整体精度更高,而且我们的网络过拟合的速度也慢得多。 如果您的计算机视觉问题只包含少量数据,那么图像增强就是您想要做的事情。
总结
在本章中,我们快速介绍了许多基础知识。 我们讨论了卷积层及其如何用于神经网络。 我们还介绍了批量规范化,池化层和数据增强。 最后,我们使用 Keras 从零开始训练卷积神经网络,然后使用数据增强对该网络进行改进。
我们还讨论了如何基于数据的渴望计算机视觉的深度神经网络问题。 在下一章中,我将向您展示迁移学习,这是我最喜欢的技术之一。 这将帮助您快速解决计算机视觉问题,并获得惊人的结果并且数据量更少。
八、将预训练的 CNN 用于迁移学习
迁移学习很棒。 实际上,在一本充满奇妙事物的书中,这可能是我必须告诉您的最奇妙的事物。 如果没有,那也许至少是我可以教给您的最有用和最实用的深度学习技术。 迁移学习可以帮助您解决深度学习问题,尤其是计算机视觉问题,而涉及问题范围的数据和数据却很少。 在本章中,我们将讨论什么是迁移学习,什么时候应该使用它,最后讨论如何在 Keras 中进行迁移学习。
我们将在本章介绍以下主题:
- 迁移学习概述
- 何时应使用迁移
- 源/目标数量和相似性的影响
- Keras 的迁移学习
迁移学习概述
在第 7 章和“卷积神经网络”中,我们训练了约 50,000 个观测值的卷积神经网络,并且由于网络和问题的复杂性,在开始训练的短短几个周期后,我们过拟合了。 如果您还记得的话,我曾评论说我们的训练集中有 50,000 个观察结果对于计算机视觉问题不是很大。 确实如此。 计算机视觉问题喜欢数据,而我们可以提供给他们的数据越多,它们的表现就越好。
我们可能认为计算机视觉技术最先进的深度神经网络通常在称为 ImageNet 的数据集上进行训练。 ImageNet数据集是包含 120 万张图像的 1,000 个分类器。 这还差不多! 如此庞大的数据集使研究人员能够构建真正复杂的深度神经网络,以检测复杂的特征。 当然,在 120 万张图像上训练有时具有 100 多个层的模型的价格很高。 训练可能需要数周和数月,而不是数小时。
但是,如果我们可以从一个最先进的,多层的,经过数百万张图像训练的网络开始,然后仅使用少量数据将该网络应用于我们自己的计算机视觉问题,该怎么办? 那就是迁移学习!
要使用迁移学习,我们将执行以下步骤:
- 从训练非常大的复杂计算机视觉问题的模型开始; 我们将其称为我们的源域
- 删除网络的最后一层(
softmax层),并可能删除其他完全连接的层 - 将最后几层替换为适合我们新问题的层,我们将其称为目标域
- 冻结所有已训练的层,使其权重不变
- 在目标域数据上训练网络
如果我们在这里停止,这通常被称为特征提取,因为我们正在使用在源域上训练的网络来提取目标域的视觉特征。 然后,我们使用栓接到该特征提取网络上的相对较小的神经网络来执行目标域任务。 根据我们的目标和数据集,这可能就足够了。
可选地,我们将通过解冻一些或所有冻结的层来微调整个网络,然后通常以很小的学习率再次进行训练。 我们将在短期内讨论何时使用微调,但是请确保我们涵盖了首先使用迁移学习的一些原因。
何时应使用迁移
当您的数据有限且存在解决类似问题的网络时,迁移学习会非常有效。 您可以使用迁移学习将最先进的网络和大量数据带入一个其他小的问题。 那么,什么时候应该使用迁移学习? 随时可以! 但是,我希望您首先考虑两个规定。 我们将在以下各节中讨论它们。
数据有限
关于计算机视觉和迁移学习,我最常被问到的问题是:我必须拥有多少张图像? 这是一个很难回答的问题,因为,正如我们将在下一节中看到的那样,更多通常更好。 一个更好的问题可能是:我可以使用几张图像来充分解决我的业务问题?
那么,我们的数据集有多有限? 尽管远非科学,但我已经建立了使用多达 2,000 张图像进行二分类任务的有用模型。 更简单的任务和更多样化的图像集通常可以在较小的数据集下获得更令人满意的结果。
根据经验,您至少需要几千张某类的图像,而通常最好使用 10 至 2 万张图像。
常见问题域
如果您的目标域至少与源域有些相似,那么迁移学习会很有效。 例如,假设您正在将图像分类为包含猫或狗。 有许多ImageNet训练有素的图像分类器非常适合用于此类型或问题。
相反,让我们想象我们的问题是将 CT 扫描或 MRI 归类为是否包含肿瘤。 此目标域与ImageNet源域非常不同。 这样,虽然使用迁移学习可能(并且可能会)有好处,但我们将需要更多的数据,并且可能需要进行一些微调才能使网络适应此目标域。
源/目标数量和相似性的影响
直到最近,很少有人研究数据量和源/目标域相似性对迁移学习表现的影响。 但是,这是一个对迁移学习的可用性很重要的主题,也是我撰写的主题。 在我的同事撰写的《调查数据量和域相似性对迁移学习应用的影响》中,对这些主题进行了一些实验。 这就是我们发现的东西。
更多数据总是有益的
Google 研究人员在《重新研究深度学习周期数据的不合理有效性》中进行的几次实验中,构建了一个内部数据集,其中包含 3 亿个观测值,显然比ImageNet大得多。 然后,他们在该数据集上训练了几种最先进的架构,从而使模型显示的数据量从 1000 万增加到 3000 万,1 亿,最后是 3 亿。 通过这样做,他们表明模型表现随用于训练的观察次数的对数线性增加,这表明在源域中,更多的数据总是有帮助。
但是目标域呢? 我们使用了一些类似于我们在迁移学习过程中可能使用的类型的数据集重复了 Google 实验,包括我们将在本章稍后使用的Dogs versus Cats数据集。 我们发现,在目标域中,模型的表现随用于训练的观察次数的对数线性增加,就像在源域中一样。 更多数据总是有帮助的。
源/目标域相似度
迁移学习的独特之处在于您担心源域和目标域之间的相似度。 经过训练以识别人脸的分类器可能不会轻易迁移到识别各种架构的目标领域。 我们进行了源和目标尽可能不同的实验,以及源和目标域非常相似的实验。 毫不奇怪,当迁移学习应用中的源域和目标域非常不同时,与相似时相比,它们需要更多的数据。 它们也需要更多的微调,因为当域在视觉上非常不同时,特征提取层需要大量的学习。
Keras 的迁移学习
与本书中的其他示例不同,在这里我们将需要涵盖目标域问题,源域问题以及我们正在使用的网络架构。 我们将从目标域的概述开始,这是我们要解决的问题。 然后,我们将介绍网络最初经过训练的源域,并简要介绍我们将使用的网络架构。 然后,我们将在本章的其余部分中将问题联系在一起。 我们需要分别考虑两个域,因为它们的大小和相似性与网络表现密切相关。 目标和源的类型越近,结果越好。
目标域概述
在本章的示例中,我将使用 Kaggle 的Dogs versus Cats数据集。 该数据集包含 25,000 张猫和狗的图像。 每个类别之间达到完美平衡,每个类别 12,500。 可以从这里下载数据集。
这是一个二分类问题。 每张照片都包含狗或猫,但不能同时包含两者。
该数据集由 Jeremy Elson 等人于 2007 年组装。 ,它目前托管在 www.kaggle.com 上。 它是完全免费下载和用于学术用途的,但是它确实需要一个 Kaggle 帐户并接受其最终用户许可。 一样,这是一个了不起的数据集,因此我在此处包括使用说明。
源域概述
我们将从在 ImageNet 上训练的深度神经网络开始。 如果您从“迁移学习概述”部分中回顾过,ImageNet是一个 1,000 类分类器,训练了大约 120 万张图像。 ImageNet数据集中都包含狗和猫的图像,因此在这种情况下,我们的目标域实际上与我们的源域非常相似。
源网络架构
我们将使用 Inception-V3 网络架构。 与您到目前为止在本书中所看到的相比,Inception 架构很有趣并且非常复杂。 如果您从第 7 章,“卷积神经网络”中回想起,我们必须围绕网络架构做出的决定之一就是选择过滤器大小。 对于每一层,我们必须决定是否应使用例如3 x 3过滤器,而不是5 x 5过滤器。 当然,也许根本就不需要另一次卷积。 也许像池化之类的东西可能更合适。 因此,如果我们在每一层都做所有事情,该怎么办? 这就是开始的动机。
该架构基于一系列模块或称为初始模块的构建块。 在每个初始模块中,先前的激活都赋予1 x 1卷积,3 x 3卷积,5 x 5卷积和最大池化层。 然后将输出连接在一起。
Inception-V3 网络由几个相互堆叠的 Inception 模块组成。 最后两层都完全连接,输出层是 1,000 个神经元 softmax。
通过使用keras.applications.inception_v3中的InceptionV3类,我们可以加载 Inception-V3 网络及其权重。 Keras 的网络动物园中有几个流行的网络,它们都位于keras.applications内部。 只需多一点工作就可以加载在 TensorFlow 中创建的模型。 也可以转换在其他架构中训练的模型,但这不在快速参考的范围之内。
要加载 Inception,我们只需要实例化一个InceptionV3对象,它是 Keras 模型,如以下代码所示:
from keras.applications.inception_v3 import InceptionV3
base_model = InceptionV3(weights='imagenet', include_top=False)
您可能会注意到,我们在这里说了include_top=False,这表明我们不需要网络的顶层。 这免除了我们手动清除它们的工作。 第一次运行此代码时,它将下载 Inception-V3 网络架构并保存权重并将其缓存给我们。 现在,我们只需要添加我们自己的完全连接的层即可。
迁移网络架构
我们将用更适合我们的用例的全连接层替换最后两层。 由于我们的问题是二分类,因此我们将使用激活sigmoid的单个神经元替换输出层,如以下代码所示:
# add a global spatial average pooling layer
x = base_model.output
x = GlobalAveragePooling2D()(x)
# let's add a fully-connected layer
x = Dense(1024, activation='relu')(x)
# and a logistic layer
predictions = Dense(1, activation='sigmoid')(x)
# this is the model we will train
model = Model(inputs=base_model.input, outputs=predictions)
注意,我们在这里使用GlobalAveragePooling2D层。 该层将前一层的 4D 输出平坦化为 2D 层,通过求平均将其适合于我们的全连接层。 通过指定pooling='avg' or 'max'来加载基本模型时,也可以完成此操作。 这是您如何处理此问题的电话。
至此,我们已经准备好训练网络。 但是,在执行此操作之前,我们需要记住冻结基本模型中的层,以免新的完全连接的层疯狂地试图学习时它们的权重不变。 为此,我们可以使用以下代码遍历各层并将其设置为不可训练:
for layer in base_model.layers:
layer.trainable = False
数据准备
我们将首先从 Kaggle 下载数据,然后将train.zip解压缩到本书的Chapter08目录中。 现在,您将拥有一个名为train/的目录,其中包含 25,000 张图像。 每个名称都将类似于cat.number.jpg。
我们想移动这些数据,以便我们为训练,验证和测试创建单独的目录。 这些目录中的每一个都应具有猫和狗的目录。 这都是非常无聊且平凡的工作,因此,我创建了data_setup.py来为您执行此操作。 一旦运行它,数据将在本章的其余部分中全部格式化。
完成后,您将拥有一个具有以下结构的数据目录:
数据输入
快速浏览图像应使您确信我们的图像的分辨率和大小均不同。 正如您从第 7 章,“卷积神经网络”,所了解的那样,我们需要这些图像的大小与神经网络的输入张量一致。 这是一个非常现实的问题,您将经常面对计算机视觉任务。 虽然当然可以使用 ImageMagick 之类的程序来批量调整图像大小,但 Keras ImageDataGenerator类可用于快速调整图像大小,这就是我们要做的。
Inception-V3 期望299 x 299 x 3图像。 我们可以在数据生成器中指定此目标大小,如以下代码所示:
train_datagen = ImageDataGenerator(rescale=1./255)
val_datagen = ImageDataGenerator(rescale=1./255)
train_generator = train_datagen.flow_from_directory(
train_data_dir,
target_size=(img_width, img_height),
batch_size=batch_size,
class_mode='binary')
validation_generator = val_datagen.flow_from_directory(
val_data_dir,
target_size=(img_width, img_height),
batch_size=batch_size,
class_mode='binary')
如果需要,我们当然可以在这里使用数据增强,但是我们实际上并不需要它。
我们在这里最有趣的事情可能是使用数据生成器的flow_from_directory()方法。 此方法采用一条路径,并根据该路径生成一批图像。 它为我们完成了将映像从磁盘中取出的所有工作。 由于它一次执行一批,因此即使不需要时,我们甚至不必将所有 50,000 个图像保留在 RAM 中。 很酷吧?
训练(特征提取)
对于此模型,我们将训练两次。 对于第一轮训练,我们将通过冻结网络的训练来进行 10 个周期的特征提取,仅调整完全连接的层权重,如我们在“迁移网络架构”部分中所讨论的。 然后,在下一部分中,我们将解冻某些层并再次进行训练,对另外 10 个周期进行微调,如以下代码所示:
data_dir = "data/train/"
val_dir = "data/val/"
epochs = 10
batch_size = 30
model = build_model_feature_extraction()
train_generator, val_generator = setup_data(data_dir, val_dir)
callbacks_fe = create_callbacks(name='feature_extraction')
# stage 1 fit
model.fit_generator(
train_generator,
steps_per_epoch=train_generator.n // batch_size,
epochs=epochs,
validation_data=val_generator,
validation_steps=val_generator.n // batch_size,
callbacks=callbacks_fe,
verbose=1)
scores = model.evaluate_generator(val_generator, steps=val_generator.n // batch_size)
print("Step 1 Scores: Loss: " + str(scores[0]) + " Accuracy: " + str(scores[1]))
在前面的示例中,我们使用ImageDataGenerator的n属性来了解可用于生成器的图像总数,并将每个周期的步骤定义为该数目除以批量大小。
此代码的其余部分应该很熟悉。
如前所述,我们只需要训练大约 10 个周期。 现在,让我们看一下 TensorBoard 中的训练过程:
如您所见,即使经过一个周期,网络的表现仍然非常好。 直到大约第 7 个阶段,我们都取得了非常微弱的表现提升。在第 7 个阶段,我们达到了最佳表现,导致 0.9828 的精度和 0.0547 的损失。
训练(微调)
为了微调网络,我们需要解冻一些冻结的层。 您可以解冻多少层,并且可以解冻任意数量的网络。 实际上,在大多数情况下,我们仅看到解冻最顶层的好处。 在这里,我仅解冻最后一个初始块,该块从图的249层开始。 以下代码描述了此技术:
def build_model_fine_tuning(model, learning_rate=0.0001, momentum=0.9):
for layer in model.layers[:249]:
layer.trainable = False
for layer in model.layers[249:]:
layer.trainable = True
model.compile(optimizer=SGD(lr=learning_rate,
momentum=momentum), loss='binary_crossentropy', metrics=
['accuracy'])
return model
另请注意,我对随机梯度下降使用的学习率非常低,因此需要进行微调。 重要的是,此时应缓慢移动重物,以免在错误的方向上发生太大的飞跃。 我不建议使用adam或rmsprop进行微调。 以下代码描述了微调机制:
callbacks_ft = create_callbacks(name='fine_tuning')
# stage 2 fit
model = build_model_fine_tuning(model)
model.fit_generator(
train_generator,
steps_per_epoch=train_generator.n // batch_size,
epochs=epochs,
validation_data=val_generator,
validation_steps=val_generator.n // batch_size,
callbacks=callbacks_ft,
verbose=2)
scores = model.evaluate_generator(val_generator, steps=val_generator.n // batch_size)
print("Step 2 Scores: Loss: " + str(scores[0]) + " Accuracy: " + str(scores[1]))
我们可以再次查看 TensorBoard 图,以了解我们在进行微调后是否能得到任何收益:
毫无疑问,我们的模型确实可以改进,但是只有很少的改进。 虽然规模很小,但您会注意到验证损失正在努力改善,并且可能显示出一些过拟合的迹象。
在这种情况下,微调几乎没有带来任何好处,但并非总是如此。 在此示例中,目标域和源域非常相似。 如前所述,由于源域和目标域不同,您从微调中获得的收益将增加。
总结
在本章中,我们介绍了迁移学习,并演示了如何使用在源域上进行预训练的网络如何极大地缩短训练时间,并最终改善我们的深度神经网络的表现。 我希望您喜欢这项技术,它是我的最爱之一,因为它非常实用,而且我通常会从中获得很好的效果。
在下一章中,我们将从计算机视觉过渡到可以记住先前输入的网络,使它们成为预测序列中下一项的理想选择。