深度学习高级应用指南(二)
三、卷积神经网络基础
在这一章,我们将看看卷积神经网络(CNN)的主要组成部分:内核和池层。然后我们将看看典型的网络是什么样子的。然后,我们将尝试用一个简单的卷积网络解决一个分类问题,并尝试将卷积运算可视化。这样做的目的是试图理解,至少是直观地理解,学习是如何进行的。
内核和过滤器
CNN 的主要组件之一是滤波器,它是具有维度nK×nK的方阵,其中 n K 是整数,并且通常是小数字,如 3 或 5。有时过滤器也被称为内核。使用内核来自经典的图像处理技术。如果你用过 Photoshop 或者类似的软件,你就习惯了做锐化、模糊、浮雕之类的操作。 1 所有这些操作都是用内核来完成的。在这一节我们将会看到内核到底是什么以及它们是如何工作的。请注意,在本书中,我们将互换使用这两个术语(内核和过滤器)。让我们定义四种不同的滤波器,并在本章后面检查它们在卷积运算中的效果。对于这些示例,我们将使用 3 × 3 滤波器。目前,只是把下面的定义作为参考,我们将在本章的后面看到如何使用它们。
-
The following kernel will allow the detection of horizontal edges
-
The following kernel will allow the detection of vertical edges
-
The following kernel will allow the detection of edges when luminosity changes drastically
-
The following kernel will blur edges in an image
在接下来的章节中,我们将使用滤镜对测试图像进行卷积,看看它们的效果如何。
盘旋
理解 CNN 的第一步是理解卷积。最简单的方法是通过几个简单的案例来看它的实际应用。首先,在神经网络的环境中,卷积是在张量之间进行的。该操作得到两个张量作为输入,并产生一个张量作为输出。操作通常用操作符*表示。
让我们看看它是如何工作的。考虑两个张量,维数都是 3 × 3。卷积运算通过应用以下公式来完成:
在这种情况下,结果仅仅是每个元素的总和, a i ,乘以各自的元素, k i 。在更典型的矩阵形式中,这个公式可以用一个双和写成
然而,第一个版本的优点是使基本思想非常清楚:来自一个张量的每个元素乘以第二个张量的对应元素(相同位置的元素),然后将所有值求和以获得结果。
在上一节中,我们谈到了核,原因是卷积通常是在张量和核之间进行的,我们可以在这里用 A 表示。典型地,核很小,3 × 3 或 5 × 5,而输入张量 A 通常更大。例如,在图像识别中,输入张量 A 是尺寸可能高达 1024 × 1024 × 3 的图像,其中 1024 × 1024 是分辨率,最后一个尺寸(3)是颜色通道的数量,即 RGB 值。
在高级应用中,图像甚至可能具有更高的分辨率。为了理解当我们有不同维数的矩阵时如何应用卷积,让我们考虑一个 4 × 4 的矩阵 A
在这个例子中,我们将取核为 3 × 3
想法是从矩阵的左上角 A 开始,选择一个 3 × 3 的区域。在这个例子中
或者,这里用粗体标记的元素:
然后,我们执行卷积,如开头所解释的,在这个更小的矩阵 A 1 和 K 之间,得到(我们将用 B 1 表示结果):
然后我们需要将一列的矩阵 A 中所选的 3 × 3 区域向右移动,并选择这里用粗体标记的元素:
这将给我们第二子矩阵 A 2 :
然后,我们再次执行这个更小的矩阵 A 2 和 K 之间的卷积:
我们不能再向右移动我们的 3 × 3 区域,因为我们已经到达了矩阵 A 的末尾,所以我们要做的是将它向下移动一行,并从左侧重新开始。下一个选择的区域将是
同样,我们执行A?? 3 与 K 的卷积
您可能已经猜到了这一点,最后一步是将我们的 3 × 3 选定区域向右移动一列,并再次执行卷积。我们选择的区域现在将是
此外,卷积将给出以下结果:
现在我们不能再移动我们的 3 × 3 区域了,无论是向右还是向下。我们计算了四个值: B 1 、 B 2 、 B 3 、 B 4 。这些元素将形成卷积运算的结果张量,给出张量 B :
当张量 A 较大时,可以应用相同的过程。你将简单地得到一个更大的结果 B 张量,但是得到元素 B i 的算法是相同的。在继续之前,我们还有一个小细节需要讨论,那就是 stride 的概念。在前面的过程中,我们总是将 3 × 3 区域向右移动一列,向下移动一行。在本例 1 中,行数和列数称为步距,通常用 s 表示。Stride s = 2 仅仅意味着我们在每一步将我们的 3 × 3 区域向右移动两列,向下移动两行。
我们需要讨论的另一件事是输入矩阵 A 中选定区域的大小。在此过程中,我们移动的选定区域的尺寸必须与所使用的内核的尺寸相同。如果你使用 5 × 5 的内核,你需要在 A 中选择一个 5 × 5 的区域。一般来说,给定一个nK×nK内核,你在 A 中选择一个nK×nK区域。
在更正式的定义中,在神经网络上下文中,与步幅 s 的卷积是这样一个过程,它取一个张量 A 的维数nA×nA和一个核Kn*K×*n**
这里我们用⌊ x ⌋表示 x 的整数部分(在编程界,这通常被称为 x 的底)。这个公式的证明需要花很长时间来讨论,但是很容易看出为什么它是正确的(试着推导它)。为了简单一点,假设 n K 是奇数。你很快就会明白为什么这很重要(虽然不是基本的)。让我们开始正式解释这个情况,步长为 1。该算法根据以下公式从输入张量 A 和核 K 生成新的张量 B
这个公式晦涩难懂。让我们再研究一些例子,以便更好地理解意思。在图 3-1 中,你可以看到卷积如何工作的直观解释。假设有一个 3 × 3 的滤镜。那么在图 3-1 中,你可以看到矩阵 A 的左上九个元素,用黑色实线画出的正方形标记,就是根据这个公式用来生成矩阵B1 的第一个元素。用虚线画的正方形标记的元素是用于生成第二个元素B2 的元素,以此类推。
图 3-1
卷积的直观解释
为了重申我们在开始的例子中讨论的内容,基本思想是将矩阵 A 的 3 × 3 平方的每个元素乘以核 K 的相应元素,并将所有数字求和。这个和就是新矩阵 B 的元素。计算出B1 的值后,将原始矩阵中一列的区域向右移动(图 3-1 中用虚线表示的方块)并重复操作。您继续向右移动区域,直到到达边界,然后向下移动一个元素,并从左侧重新开始。你继续以这种方式,直到矩阵的右下角。相同的内核用于原始矩阵中的所有区域。
以内核为例,你可以在图 3-2 中看到 A 的哪些元素乘以
中的哪些元素,元素B1 的结果就是所有乘法的总和
图 3-2
与内核卷积的可视化
在图 3-3 中,可以看到步长 s = 2 的卷积示例。
图 3-3
步幅为 s = 2 的卷积的直观解释
输出矩阵的维数只占的底(整数部分)
在图 3-4 中可以直观的看到。如果 s > 1,根据 A 的尺寸,可能发生的情况是,在某一点上你不能再在矩阵 A (例如你在图 3-3 中看到的黑色方块)上移动你的窗口,并且你不能完全覆盖矩阵 A 的全部。在图 3-4 中,您可以看到如何在矩阵 A (标有许多 X)的右侧需要一个额外的列来执行卷积运算。在图 3-4 中,我们选择了 s = 3,由于我们有nA= 5 和nK= 3,因此 B 将是一个标量。
图 3-4
直观解释为什么在评估生成的矩阵 B 尺寸时需要 floor 函数
从图 3-4 中你可以很容易地看到,一个 3 × 3 的区域,只能覆盖 A 的左上区域,由于步长 s = 3,你会在 A 之外结束,因此可以只考虑一个区域进行卷积运算。因此,你最终得到了一个标量张量 B 。
现在让我们看几个额外的例子,让这个公式更加清晰。先说一个 3 × 3 的小矩阵
此外,让我们考虑内核
步幅 s = 1。卷积将由下式给出
而且,结果 B 会是一个标量,因为nA= 3,nK= 3。
如果你考虑一个维数为 4 × 4 的矩阵 A ,或者nA= 4,nK= 3, s = 1,你将得到维数为 2 × 2 的矩阵 B ,因为
例如,您可以验证给定的
和
我们有步距为s= 1
我们用我给你的公式来验证其中一个元素: B 11 。我们有
请注意,我给你的卷积公式仅适用于步长 s = 1,但可以很容易地推广到其他值的 s 。
这个计算很容易用 Python 实现。对于 s = 1,下面的函数可以足够容易地计算两个矩阵的卷积(您可以在 Python 中使用现有的函数来完成,但我认为从头开始看如何做是有启发性的):
import numpy as np
def conv_2d(A, kernel):
output = np.zeros([A.shape[0]-(kernel.shape[0]-1), A.shape[1]-(kernel.shape[0]-1)])
for row in range(1,A.shape[0]-1):
for column in range(1, A.shape[1]-1):
output[row-1, column-1] = np.tensordot(A[row-1:row+2, column-1:column+2], kernel)
return output
注意,输入矩阵 A 甚至不需要是平方矩阵,但是假设内核是并且它的维数nK是奇数。可以用下面的代码评估前面的示例:
A = np.array([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]])
K = np.array([[1,2,3],[4,5,6],[7,8,9]])
print(conv_2d(A,K))
这给出了结果:
[[ 348\. 393.]
[ 528\. 573.]]
卷积的例子
现在,让我们尝试将我们在开始时定义的内核应用到一个测试图像中,看看结果。作为测试图像,让我们用代码创建一个尺寸为 160 × 160 像素的棋盘:
chessboard = np.zeros([8*20, 8*20])
for row in range(0, 8):
for column in range (0, 8):
if ((column+8*row) % 2 == 1) and (row % 2 == 0):
chessboard[row*20:row*20+20, column*20:column*20+20] = 1
elif ((column+8*row) % 2 == 0) and (row % 2 == 1):
chessboard[row*20:row*20+20, column*20:column*20+20] = 1
在图 3-5 中,可以看到棋盘的样子。
图 3-5
用代码生成的棋盘图像
现在让我们用步长为 s = 1 的不同内核对该图像进行卷积。
使用内核,将检测水平边缘。这可以应用于代码
edgeh = np.matrix('1 1 1; 0 0 0; -1 -1 -1')
outputh = conv_2d (chessboard, edgeh)
在图 3-6 中,您可以看到输出的样子。使用以下代码可以很容易地生成图像:
图 3-6
在内核和棋盘图像之间执行卷积的结果
Import matplotlib.pyplot as plt
plt.imshow(outputh)
现在你可以理解为什么这个内核检测水平边缘了。此外,这个内核检测你什么时候从亮到暗,反之亦然。注意,正如所料,这张图片只有 158 × 158 像素,因为
现在让我们使用这段代码来应用:
edgev = np.matrix('1 0 -1; 1 0 -1; 1 0 -1')
outputv = conv_2d (chessboard, edgev)
这给出了如图 3-7 所示的结果。
图 3-7
在内核和棋盘图像之间执行卷积的结果
现在我们可以使用内核:
edgel = np.matrix ('-1 -1 -1; -1 8 -1; -1 -1 -1')
outputl = conv_2d (chessboard, edgel)
这给出了如图 3-8 所示的结果。
图 3-8
在内核和棋盘图像之间执行卷积的结果
此外,我们可以应用模糊内核:
edge_blur = -1.0/9.0*np.matrix('1 1 1; 1 1 1; 1 1 1')
output_blur = conv_2d (chessboard, edge_blur)
在图 3-9 中,你可以看到两幅图——左边是模糊图像,右边是原始图像。这些图像只显示了原始棋盘的一小部分区域,以使模糊更加清晰。
图 3-9
模糊内核的效果左边是模糊图像,右边是原始图像。
为了结束这一部分,让我们试着更好地理解如何检测边缘。考虑具有急剧垂直过渡的以下矩阵,因为左边部分全是 10,右边部分全是 0。
ex_mat = np.matrix('10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0; 10 10 10 10 0 0 0 0')
这看起来像这样
matrix([[10, 10, 10, 10, 0, 0, 0, 0],
[10, 10, 10, 10, 0, 0, 0, 0],
[10, 10, 10, 10, 0, 0, 0, 0],
[10, 10, 10, 10, 0, 0, 0, 0],
[10, 10, 10, 10, 0, 0, 0, 0],
[10, 10, 10, 10, 0, 0, 0, 0],
[10, 10, 10, 10, 0, 0, 0, 0],
[10, 10, 10, 10, 0, 0, 0, 0]])
我们来考虑一下内核。我们可以用这段代码执行卷积:
ex_out = conv_2d (ex_mat, edgev)
结果如下:
array([[ 0., 0., 30., 30., 0., 0.],
[ 0., 0., 30., 30., 0., 0.],
[ 0., 0., 30., 30., 0., 0.],
[ 0., 0., 30., 30., 0., 0.],
[ 0., 0., 30., 30., 0., 0.],
[ 0., 0., 30., 30., 0., 0.]])
在图 3-10 中,可以看到原始矩阵(左边)和右边卷积的输出。与内核的卷积已经清楚地检测到原始矩阵中的急剧转变,在从黑到白的转变发生的地方用垂直黑线标记。例如,考虑B11= 0
注意,在输入矩阵中
没有过渡,因为所有值都是相同的。相反,如果你考虑B13 你需要考虑输入矩阵的这个区域
其中有一个明显的过渡,因为最右列由 0 和其余的 10 组成。你现在得到一个不同的结果
此外,这正是一旦水平方向上的值有显著变化,卷积就返回高值的原因,因为在核中乘以列 1 的值将更有意义。当沿着水平轴存在从小到大的值的转变时,乘以-1 的元素将给出绝对值更大的结果。因此,最终结果将是负的,绝对值很大。这就是为什么这个内核也可以检测到你是否从一个浅色到一个深色,反之亦然。如果您考虑不同假设矩阵中的相反转变(从 0 到 10),您将会
我们沿着水平方向从 0 移动到 10。
图 3-10
如文本中所述,矩阵ex_mat与核的卷积结果
请注意,正如所料,输出矩阵的维数是 5 × 5,因为原始矩阵的维数是 7 × 7,而核是 3 × 3。
联营
池化是 CNN 的第二个基本操作。这个运算比卷积容易理解得多。为了理解它,让我们看一个具体的例子,并考虑什么叫做 *max pooling。*再次考虑我们在卷积讨论中讨论过的 4 × 4 矩阵:
为了执行最大池,我们需要定义一个大小为nK×nK的区域,类似于我们对卷积所做的。我们来考虑一下nK= 2。我们需要做的是从我们的矩阵左上角的 A 开始,选择一个nK×nK区域,在我们的例子中是从 A 的 2 × 2。在这里,我们将选择
或者,矩阵中以粗体标记的元素 A 在此:
从选择的元素中, a 1 , a 2 , a 5 和 a 6 ,最大汇集运算选择最大值。结果用B1 表示
然后,我们需要将 2 × 2 窗口向右移动两列,通常与所选区域的列数相同,并选择以粗体标记的元素:
或者换句话说,更小的矩阵
然后,最大池算法将选择这些值中的最大值,并给出一个用 B 2 表示的结果
此时,我们不能再将 2 × 2 区域向右移动,所以我们将其向下移动两行,并从 A 的左侧再次开始该过程,选择以粗体标记的元素并获得最大值,将其命名为 B 3 。
在这种情况下,步距 s 与我们在卷积中已经讨论过的意义相同。它只是在选择元素时移动区域的行数或列数。最后,我们选择 A 底部的最后一个区域 2 × 2,选择元素A11、A12、A15 和 a 16 。然后我们得到最大值,称之为B4。利用我们在此过程中获得的值,在本例中是四个值 B 1 、 B 、 2 、 B 、 3 和 B 、 4 ,我们将构建一个输出张量:
在这个例子中,我们有 s = 2。基本上,该操作将矩阵 A 、步距 s 和内核大小 n K (我们在之前的示例中选择的区域的维度)作为输入,并返回新的矩阵 B ,其维度由我们针对卷积讨论的相同公式给出:
为了重申这个想法,从矩阵 A 的左上角开始,取一个维度为nK×nK的区域,对所选元素应用 max 函数,然后向右移动 s 元素的区域,再次选择一个维度为 n K 的新区域在图 3-11 中,您可以看到如何从步长为 s = 2 的矩阵 A 中选择元素。
图 3-11
步长为 s = 2 的池的可视化
例如,对输入 A 应用最大池化
会给你这个结果(很容易验证):
因为四是用粗体标记的值的最大值。
11 是这里用粗体标记的最大值:
诸如此类。值得一提的是另一种池化方法,尽管它没有 max-pooling 使用得那么广泛: 平均池化 。它不是返回所选值的最大值,而是返回平均值。
注意
最常用的池操作是最大池。平均池的使用并不广泛,但可以在特定的网络架构中找到。
填料
这里值得一提的是填充。有时,在处理图像时,从维度不同于原始图像的卷积运算中获得结果并不是最佳选择。这时需要填充。这个想法很简单:在最终图像的顶部和底部添加像素行,在右侧和左侧添加像素列,这样得到的矩阵与原始矩阵大小相同。一些策略用零填充添加的像素,用最接近的像素的值填充,等等。例如,在我们的例子中,带有零填充的ex_out矩阵如下所示
array([[ 0., 0., 0., 0., 0., 0., 0., 0.],
[ 0., 0., 0., 30., 30., 0., 0., 0.],
[ 0., 0., 0., 30., 30., 0., 0., 0.],
[ 0., 0., 0., 30., 30., 0., 0., 0.],
[ 0., 0., 0., 30., 30., 0., 0., 0.],
[ 0., 0., 0., 30., 30., 0., 0., 0.],
[ 0., 0., 0., 30., 30., 0., 0., 0.],
[ 0., 0., 0., 0., 0., 0., 0., 0.]])
仅作为参考,在使用填充符 p (用作填充符的行和列的宽度)的情况下,在卷积和合并的情况下,矩阵 B 的最终尺寸由下式给出
注意
当处理真实图像时,你总是有彩色图像,用三个通道编码:RGB。这意味着卷积和合并必须在三个维度上完成:宽度、高度和颜色通道。这将增加算法的复杂性。
CNN 的构建模块
卷积和汇集操作用于构建 CNN 中使用的层。在 CNN 中,您通常可以找到以下层
-
卷积层
-
池层
-
完全连接的层
全连接层正是我们在前面所有章节中看到的:一个层,其中的神经元与前一层和后一层的所有神经元相连。你已经认识他们了。另外两个需要一些额外的解释。
卷积层
卷积层将张量(由于三个颜色通道,它可以是三维的)作为输入,例如图像,应用特定数量的核,通常是 10、16 或更多,添加偏差,应用 ReLu 激活函数(例如)以将非线性引入卷积的结果,并产生输出矩阵 B 。
在前面的章节中,我展示了一些用一个内核应用卷积的例子。如何同时应用几个内核?答案很简单。最终的张量(我现在使用张量这个词,因为它不再是一个简单的矩阵) B 将不是二维而是三维。让我们用nc来表示您想要申请的内核数量(由于有时人们会谈到通道,所以会使用 c )。您只需将每个过滤器独立应用于输入,并将结果堆叠起来。因此,你得到的不是一个维数为 n*B×nB×的单一矩阵BnB×BnBB这意味着这*
*将是输入图像与第一内核的卷积的输出,以及
将是与第二个内核卷积的输出,以此类推。卷积层只是将输入转换成输出张量。然而,这一层的权重是什么呢?网络在训练阶段学习的权重或参数是内核本身的元素。我们讨论过我们有 n c 个内核,每个nK×nK个维度。这意味着卷积层中有参数。
注意
卷积层中的参数数量与输入图像大小无关。这个事实有助于减少过度拟合,尤其是在处理大输入图像时。
有时这一层用单词POOL表示,然后是一个数字。在我们的例子中,我们可以用POOL1来表示这个层。在图 3-12 中,你可以看到一个卷积层的示意图。通过应用与维度为nA×nA×nc的张量中的nc核的卷积来变换输入图像。
图 3-12
卷积层的表示 2
当然,卷积层不一定紧接在输入之后。当然,卷积层可以将任何其他层的输出作为输入。请记住,输入图像通常会有尺寸nA×nA×3,因为彩色图像有三个通道:红色、绿色和蓝色。在考虑彩色图像时,对 CNN 中张量的完整分析超出了本书的范围。在图中,层通常被简单地表示为立方体或正方形。
池层
池层通常用POOL和一个数字表示:例如POOL1。它将一个张量作为输入,在将池应用于输入后,给出另一个张量作为输出。
注意
一个池层没有需要学习的参数,但是它引入了额外的超参数: n K 和 stride v 。通常,在池化层中,不使用任何填充,因为使用池化的原因之一通常是为了减少张量的维数。
将层堆叠在一起
在 CNN 中,你通常将卷积层和池层堆叠在一起。一个接一个。在图 3-13 中,您可以看到一个卷积层和一个池层堆栈。卷积层之后总是有一个池层。有时这两层合在一起被称为层。原因是池层没有可学习的权重,因此它仅仅被视为与卷积层相关联的简单操作。因此,当你阅读报纸或博客时,要注意并检查他们的意图。
图 3-13
如何堆叠卷积层和池层的表示
在图 3-14 中总结 CNN 的这一部分,你可以看到一个 CNN 的例子。在图 3-14 中,你可以看到一个非常著名的 LeNet-5 网络的例子,你可以在这里阅读更多内容: https://goo.gl/hM1kAL 。您有输入,然后两次卷积池层,然后三个完全连接的层,然后一个输出层,用一个softmax激活函数来执行多类分类。我在图中放了一些指示性的数字,让你对不同层的大小有个概念。
图 3-14
类似于著名的 LeNet-5 网络的 CNN 的代表
CNN 中的权重数
指出 CNN 中的权重在不同层中的位置是很重要的。
卷积层
在卷积层中,学习的参数是滤波器本身。例如,如果您有 32 个滤波器,每个尺寸为 5×5,您将获得 32×5×5 = 832 个可学习参数,因为对于每个滤波器,您还需要添加一个偏差项。请注意,这个数字不取决于输入图像的大小。在典型的前馈神经网络中,第一层中的权重数取决于输入大小,但在这里不是这样。
一般来说,卷积层中的权重数由下式给出:
汇集层
池层没有可学习的参数,正如前面提到的,这是它通常与卷积层相关联的原因。在这一层(操作)中,没有可学习的权重。
致密层
在这一层中,权重是您从传统的前馈网络中知道的权重。所以数量取决于神经元的数量以及前一层和后一层的神经元数量。
注意
CNN 中唯一具有可学习参数的层是卷积层和致密层。
CNN: MNIST 数据集的例子
让我们从一些编码开始。我们将开发一个非常简单的 CNN,并尝试在 MNIST 数据集上进行分类。从第章到第章,你现在应该对数据集非常了解了。
像往常一样,我们首先导入必要的包:
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPool2D
from keras.utils import np_utils
import numpy as np
import matplotlib.pyplot as plt
在开始加载数据之前,我们需要一个额外的步骤:
from keras import backend as K
K.set_image_dim_ordering('th')
原因如下。为模型加载图像时,您需要将它们转换为张量,每个张量都有三个维度:
-
沿 x 轴的像素数
-
沿 y 轴的像素数
-
颜色通道的数量(在灰色图像中,此数量为;如果您有彩色图像,这个数字是 3,每个 RGB 通道一个)
在做卷积时,Keras 必须知道它在哪个轴上找到信息。特别是,定义颜色通道维度的索引是第一个还是最后一个是相关的。为了实现这一点,我们可以用keras.backend.set_image_dim_ordering()来定义数据的排序。该函数接受一个字符串作为输入,该字符串可以有两个可能的值:
-
'th'(对于库 Theano 使用的约定):Theano 期望通道维度是第二个(第一个将是观察指标)。 -
'tf'(TensorFlow 使用的约定):tensor flow 期望通道维度是最后一个。
您可以使用这两种方法,但是在准备数据时要注意使用正确的约定。否则,你会得到关于张量维数的错误信息。在接下来的内容中,我们将转换张量中的图像,颜色通道维度作为第二个维度,稍后你会看到。
现在,我们准备用以下代码加载 MNIST 数据:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
该代码将交付“扁平化”的图像,这意味着每个图像将是一个包含 784 个元素(28x28)的一维向量。我们需要将它们重塑为合适的图像,因为我们的卷积层需要图像作为输入。之后,我们需要标准化数据(记住图像是灰度的,每个像素可以有一个从 0 到 255 的值)。
X_train = X_train.reshape(X_train.shape[0], 1, 28, 28).astype('float32')
X_test = X_test.reshape(X_test.shape[0], 1, 28, 28).astype('float32')
X_train = X_train / 255.0
X_test = X_test / 255.0
请注意,既然我们已经将排序定义为'th',那么通道的数量(在本例中为 1)就是X数组的第二个元素。下一步,我们需要对标签进行一次性热编码:
y_train = np_utils.to_categorical(y_train)
y_test = np_utils.to_categorical(y_test)
我们知道我们有 10 个类,所以我们可以简单地定义它们:
num_classes = 10
现在让我们定义一个函数来创建和编译我们的 Keras 模型:
def baseline_model():
# create model
model = Sequential()
model.add(Conv2D(32, (5, 5), input_shape=(1, 28, 28), activation="relu"))
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation="relu"))
model.add(Dense(num_classes, activation="softmax"))
# Compile model
model.compile(loss='categorical_crossentropy', optimizer="adam", metrics=['accuracy'])
return model
你可以在图 3-15 中看到这个 CNN 的示意图。
图 3-15
描述我们在文中使用的 CNN 的图表。这些数字是每一层产生的张量的维数。
为了确定我们有哪种模型,我们简单地使用model.summary()调用。让我们首先创建一个模型,然后检查它:
model = baseline_model()
model.summary()
输出(查看图 3-15 中的图表)如下:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 32, 24, 24) 832
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 32, 12, 12) 0
_________________________________________________________________
dropout_1 (Dropout) (None, 32, 12, 12) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 4608) 0
_________________________________________________________________
dense_1 (Dense) (None, 128) 589952
_________________________________________________________________
dense_2 (Dense) (None, 10) 1290
=================================================================
Total params: 592,074
Trainable params: 592,074
Non-trainable params: 0
如果您想知道为什么 max-pooling 层产生 12x12 尺寸的张量,原因是因为我们没有指定跨距,Keras 将把过滤器的尺寸作为标准值,在我们的例子中是 2x2。步长为 2 的输入张量为 24x24,您将得到 12x12 的张量。
这个网络相当简单。在模型中,我们只定义了一个卷积和池化层,我们添加了一点 dropout,然后添加了一个具有 128 个神经元的密集层,然后为具有 10 个神经元的softmax添加了一个输出层。现在我们可以简单地用fit()方法训练它:
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=1, batch_size=200, verbose=1)
这将只训练一个时期的网络,并且应该给出如下输出(您的数字可能会稍有不同):
Train on 60000 samples, validate on 10000 samples
Epoch 1/1
60000/60000 [==============================] - 151s 3ms/step - loss: 0.0735 - acc: 0.9779 - val_loss: 0.0454 - val_acc: 0.9853
我们已经达到了良好的精度,没有任何过度拟合。
注意
当您将优化器参数传递给compile()方法时,Keras 将使用它的标准参数。如果您想要更改它们,您需要单独定义一个优化器。例如,要指定一个起始学习率为 0.001 的 Adam 优化器,可以使用AdamOpt = adam(lr=0.001),然后用model.compile(optimizer=AdamOpt, loss="categorical_crossentropy", metrics=['accuracy'])将其传递给编译方法。
CNN 学习的可视化
简单题外话:keras.backend.function()
有时从计算图中获得中间结果是有用的。例如,出于调试目的,您可能对特定层的输出感兴趣。在低级 TensorFlow 中,您可以简单地在会话中评估图中的相关节点,但要理解如何在 Keras 中执行并不那么容易。为了找到答案,我们需要考虑 Keras 后端是什么。最好的解释方式就是引用官方文献( https://keras.io/backend/ ):
Keras 是一个模型级的库,为开发深度学习模型提供高级的构建模块。它本身不处理张量积、卷积等低级运算。相反,它依赖于一个专门的、优化良好的张量操作库来完成,充当 Keras 的“后端引擎”。
为了完整起见,需要注意的是 Keras 使用了三个后端:TensorFlow 后端、?? 后端和 ?? 后端。当您想要编写自己的特定函数时,您应该使用抽象的 Keras 后端 API,它可以用以下代码加载:
from keras import backend as K
理解如何使用 Keras 后端超出了本书的范围(记住本书的重点不是 Keras),但是我建议你花一些时间去了解它。可能会很有用。例如,要在使用 Keras 时重置会话,可以使用以下命令:
keras.backend.clear_session()
这一章我们真正感兴趣的是在后面提供的一个具体方法:function()。其论点如下:
-
输入:占位符张量列表
-
输出:输出张量列表
-
更新:更新操作列表
-
**kwargs:传递到tf.Session.run
在本章中,我们将只使用前两个。为了理解如何使用它们,让我们以前面几节中创建的模型为例:
model = Sequential()
model.add(Conv2D(32, (5, 5), input_shape=(1, 28, 28), activation="relu"))
model.add(MaxPool2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(128, activation="relu"))
model.add(Dense(num_classes, activation="softmax"))
例如,我们如何获得第一个卷积层的输出?我们可以通过创建一个函数轻松做到这一点:
get_1st_layer_output = K.function([model.layers[0].input],[model.layers[0].output])
这将使用以下参数
-
输入:
model.layers[0].input,这是我们网络的输入 -
outputs:
model.layers[0].output,第一层的输出(索引为 0)
给定一组特定的输入,您只需让 Keras 评估您的计算图中的特定节点。注意到目前为止我们只定义了一个函数。现在我们需要将它应用到特定的数据集。例如,如果我们想将它应用到一个单一的图像,我们可以这样做:
layer_conv_output = get_1st_layer_output([np.expand_dims(X_test[21], axis=0)])[0]
这个多维数组的维数(1, 32, 24, 24)和预期的一样:一个图像,32 个过滤器,24x24 输出。在下一节中,我们将使用该函数来查看网络中学习过的滤波器的效果。
内核效应
有趣的是,可以看到学习后的核对输入图像的影响。为此,让我们从测试数据集中取一个图像(如果您打乱了数据集,您可能会在索引 21 处得到一个不同的数字)。
tst = X_test[21]
注意这个数组是如何拥有维度(1,28,28)的。这是一个六,如图 3-16 所示。
图 3-16
测试数据集中的第一个图像
为了获得第一层(卷积层)的效果,我们可以使用下面的代码(在前一节中解释过)
get_1st_layer_output = K.function([model.layers[0].input],[model.layers[0].output])
layer_conv_output = get_1st_layer_output([tst])[0]
请注意layer_conv_output是一个多维数组,它将包含输入图像与每个滤波器的卷积,相互堆叠。它的维数是(1,32,24,24)。第一个数字是 1,因为我们仅将该层应用于一个单个图像,第二个数字是 32,是我们拥有的过滤器的数量,第二个数字是 24,因为如我们所讨论的,conv层的输出张量维数由下式给出
此外,在我们的情况下
图 3-17
测试图像(a 6)与网络学习的前 12 个过滤器进行了卷积
既然在我们的网络中,我们有 n A = 28, p = 0,nK= 5,步距 s = 1。在图 3-17 中,你可以看到我们的测试图像与前 12 个滤波器(32 个对于一个图来说太多了)进行了卷积。
从图 3-17 中,您可以看到不同的过滤器如何学习检测不同的特征。例如,第三个过滤器(如图 3-18 所示)学会了检测对角线。
图 3-18
测试图像与第三滤波器卷积。它学会了检测对角线。
其他过滤器学习检测水平线或其他特征。
最大池效应
下一步是将最大池应用于卷积层的输出。正如我们所讨论的,这将减少张量的维数,并试图(直观地)浓缩相关信息。
在图 3-19 中,您可以看到来自前 12 个滤波器的张量输出。
图 3-19
当应用于来自卷积层的前 12 个张量时,池层的输出
让我们看看我们的测试图像是如何通过一个过滤器从一个卷积层和池层进行转换的(考虑第三个,只是为了说明)。在图 3-20 中可以很容易地看到效果。
图 3-20
数据集(图 a)中的原始测试图像;与第三学习滤波器卷积的图像(b 图);在最大池图层之后,使用第三个滤镜进行卷积的图像(面板 c)
请注意图像的分辨率是如何变化的,因为我们没有使用任何填充。在下一章中,我们将看看更复杂的架构,称为盗梦网络 ,,它们在处理图像时比传统的 CNN(我们在本章中已经描述过)工作得更好。事实上,简单地增加越来越多的卷积层不会轻易提高预测的准确性,而更复杂的架构会更有效。
既然我们已经看到了 CNN 最基本的组成部分,我们准备进入一些更高级的话题。在下一章中,我们将探讨许多令人兴奋的话题,如初始网络、多重损失函数、自定义损失函数和迁移学习。
Footnotes 1你可以在维基百科的 https://en.wikipedia.org/wiki/Kernel_(image_processing) 找到一个很好的概述。
2
猫图片来源: https://www.shutterstock.com/
*
四、高级 CNN 和迁移学习
在这一章中,我们来看看在开发 CNN 时通常使用的更高级的技术。特别是,我们将看到一个非常成功的新卷积网络,称为初始网络,它基于并行而不是顺序完成几个卷积运算的思想。然后,我们将看看如何使用多重成本函数,其方式与多任务学习中的方式类似。下一节将向您展示如何使用 Keras 提供的预训练网络,以及如何使用迁移学习来针对您的特定问题调整这些预训练网络。在本章的最后,我们将研究一种实现迁移学习的技术,这种技术在处理大数据集时非常有效。
多通道卷积
在前一章中,你学习了卷积是如何工作的。在示例中,我们已经明确描述了当输入是二维矩阵时如何执行它。但现实并非如此。例如,输入张量可以表示彩色图像,因此将具有三维:在 x 方向上的像素数量(沿着 x 轴的分辨率)、在 y 方向上的像素数量(沿着 y 轴的分辨率)、以及颜色通道的数量,当处理 RGB 图像时是三个(一个通道用于红色,一个用于绿色,一个用于蓝色)。情况可能会更糟。一个具有 32 个核的卷积层,每个核为 5 × 5,当期望输入每个为 28 × 28 的图像时(参见上一章中的 MNIST 示例),将具有维数为( m ,32,24,24)的输出,其中 m 是训练图像的数量。这意味着我们的卷积必须用 32 × 24 × 24 的张量来完成。那么我们如何对三维张量进行卷积运算呢?嗯,其实很简单。从数学上讲,如果内核 K 有维度nK×nK×nc,输入张量 A 有维度 n x
这意味着我们将对通道维度求和。在 Keras 中,当您在 2D 定义卷积层时,可以使用以下代码:
Conv2D(32, (5, 5), input_shape=(1, 28, 28), activation="relu")
其中第一个数字(32)是过滤器的数量,而(5,5)定义了内核的尺寸。Keras 没有告诉你的是,它自动取核 n c × 5 × 5 其中 n c 是输入张量的通道数。这就是为什么你需要给第一层input_shape参数。该信息中包含通道数。但是这三个数字哪个是正确的呢?Keras 怎么知道在这种情况下正确的是 1 而不是 28?
让我们更深入地看看我们在前一章中看到的具体例子。假设我们用以下代码导入 MNIST 数据集:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
在前一章中,我们用
X_train = X_train.reshape(X_train.shape[0], 1, 28, 28).astype('float32')
您会注意到,我们在 28 的 x 和 y 维度之前添加了一个维度 1。1 是图像中的通道数:因为它是灰度图像,所以只有一个通道。但是我们也可以在 28 的 x 和 y 尺寸之后增加通道的数量。这是我们的选择。我们可以用我们在第三章中讨论的代码告诉 Keras 采用哪个维度:
K.set_image_dim_ordering('th')
这一行很重要,因为 Keras 需要知道哪一个是信道维度,以便能够为卷积运算提取正确的信道维度。记住,对于内核,我们只指定了 x 和 y 维度,所以 Keras 需要自己找到第三维:在这个例子中是 1。你会记得,'th'的值会期望通道尺寸在 x 、 y 尺寸之前,而'tf'的值会期望通道尺寸是最后一个。所以,这只是一个保持一致的问题。您用上面的代码告诉 Keras,通道维度在哪里,然后相应地修改您的数据。让我们考虑几个额外的例子来使这个概念更加清晰。
让我们假设当使用 MNIST 图像作为输入时,我们考虑具有set_image_dim_ordering('th')(我们将忽略观察数量的维度 m )的以下网络:
Input tensors shape: 1×28×28
Convolutional Layer 1 with 32 kernels, each 5×5: output shape 32×24×24
Convolutional Layer 2 with 16 kernels, each 3×3: output shape 16×22×22
第二卷积层中的核将具有 32 × 3 × 3 的维度。来自第一卷积层(32)的信道数量在确定第二卷积层的输出维度中不起作用,因为我们对该维度求和。事实上,如果我们将第一层中的内核数量更改为 128,我们会得到以下维度:
Input tensors shape: 1×28×28
Convolutional Layer 1 with 32 kernels, each 5×5: output shape 128×24×24
Convolutional Layer 2 with 16 kernels, each 3×3: output shape 16×22×22
如您所见,第二层的输出尺寸没有任何变化。
注意
Keras 在创建过滤器时会自动推断通道尺寸,因此您需要使用set_image_dim_ordering()告诉 Keras 哪个是正确的尺寸,然后相应地调整您的数据。
为什么 1 × 1 卷积会降低维数
在这一章中,我们将研究初始网络,我们将使用 1 × 1 核,理由是这样可以降低维数。乍一看,这似乎有悖常理,但您需要记住上一节的讨论,即过滤器总是有第三维的。考虑以下一组层:
Input tensors shape: 1 × 28 × 28
Convolutional Layer 1 with 32 kernels, each 5 × 5: output shape 128 × 24 × 24
Convolutional Layer 2 with 16 kernels, each 1 × 1: output shape 16 × 24 × 24
注意具有 1 × 1 核的层是如何降低前一层的维度的。它将尺寸从 128 × 24 × 24 更改为 16 × 24 × 24。1 × 1 内核不会改变张量的 x 、 y 维度,但会改变通道维度。这就是为什么,如果你阅读关于盗梦空间网络的博客或书籍,你会读到这些核被用来减少张量的维数。
核 1 × 1 不改变张量的 x 、 y 维度,但会改变通道维度。这就是为什么它们经常被用来降低流经网络的张量的维数。
初始网络的历史和基础
初始网络最初是在 Szegedy 等人的一篇著名论文中提出的,这篇论文的标题是用卷积深入。 1 我们将详细讨论的这种新架构是在不增加计算预算的情况下,努力在图像识别任务中获得更好结果的结果。 2 添加越来越多的层将创建具有越来越多参数的模型,这将越来越困难并且训练缓慢。此外,作者希望找到一些方法,可以用在功能可能不如大型数据中心中使用的机器上。正如他们在论文中所述,他们的模型被设计为保持“推理时 15 亿乘加的计算预算”。重要的是,推断是廉价的,因为这样就可以在功能不那么强大的设备上进行;比如在手机上。
请注意,本章的目标不是分析关于初始网络的整篇原始论文,而是解释已经使用的新构建模块和技术,并向您展示如何在您的项目中使用它们。为了开发初始网络,我们将需要开始使用功能性 Keras APIs,使用多个损失函数,并使用并行而非顺序评估的层对数据集执行操作。我们也不会查看该架构的所有变体,因为这只会要求我们列出一些论文的结果,而不会给读者带来任何额外的价值(阅读原始论文会更好)。如果你有兴趣,我能给你最好的建议就是下载下来,研究一下原论文。你会在那里找到很多有趣的信息。但在本章结束时,你将拥有真正理解这些新网络的工具,并能够用 Keras 开发一个。
让我们回到“经典的”CNN。通常,这些都有一个标准的结构:堆叠的卷积层(当然有池),后面是一组密集层。很容易通过增加层数、内核数量或大小来获得更好的结果。这导致过拟合问题,因此需要大量使用正则化技术(如 dropout)来解决这个问题。更大的尺寸(在层数和内核尺寸和数量方面)当然意味着更大数量的参数,因此需要越来越高的计算资源。总而言之,“经典”CNN 的一些主要问题如下:
-
获得正确的内核大小非常困难。每个图像都不一样。一般来说,较大的内核适合于全球分布的信息,较小的内核适合于本地分布的信息。
-
深度 CNN 容易过度拟合。
-
具有许多参数的网络的训练和推断是计算密集型的。
初始模块:天真的版本
为了克服这些困难,Szegedy 和论文合著者的主要思想是并行地执行与多尺寸核的卷积,以便能够同时检测不同尺寸的特征,而不是一层接一层地顺序添加卷积。据说这些类型的网络会变得更宽,而不是更深,而不是 ??。
比如我们可能同时并行的用 1 × 1,3 × 3,5 × 5 核做卷积,甚至 max pooling,而不是一个接一个的加几个卷积层。在图 4-1 中,你可以看到不同的卷积是如何在所谓的天真的初始模块中并行完成的。
图 4-1
并行完成不同内核大小的不同卷积。这是在初始网络中使用的基本模块,称为初始模块。
在图 4-1 的例子中,1 × 1 内核将查看非常定位的信息,而 5 × 5 内核将能够发现更多的全局特征。在下一节中,我们将看看如何使用 Keras 来开发这一功能。
初始模块中的参数数量
让我们看看盗梦空间和经典 CNN 在参数数量上的差异。假设我们考虑图 4-1 中的例子。假设“前一图层”是包含 MNIST 数据集的输入图层。为了便于比较,我们将对所有层或卷积运算使用 32 个内核。在初始模块中,每个卷积运算的参数数量为
-
1 × 1 卷积:64 个参数 3 个
-
3 × 3 卷积:320 个参数
-
5 × 5 卷积:832 个参数
请记住,最大池操作没有可学习的参数。我们总共有 1216 个可学习的参数。现在,让我们假设我们创建了一个具有三个卷积层的网络,一个接一个。第一个具有 32 个 1 × 1 核,然后一个具有 32 个 3 × 3 核,最后一个具有 32 个 5 × 5 核。现在,各层中的参数总数将为(记住,例如,具有 32 个 3 × 3 内核的卷积层将具有 32 个 1 × 1 内核的卷积层的输出作为输入):
-
1 × 1 卷积层:64 个参数
-
具有 3 × 3 卷积的层:9248 个参数
-
具有 5 × 5 卷积的层:25632 个参数
总共有 34944 个可学习参数。参数数量大约是初始版本的 30 倍。您可以很容易地看到,这种并行处理大大减少了模型必须学习的参数数量。
具有降维的初始模块
在天真的初始模块中,相对于经典 CNN,我们得到的可学习参数数量较少,但实际上我们可以做得更好。我们可以在适当的地方使用 1 × 1 卷积(主要是在高维卷积之前)来降低维数。这允许我们在不增加计算预算的情况下使用越来越多的这种模块。在图 4-2 中,你可以看到这样一个模块的样子。
图 4-2
降维的初始模块示例
看到我们在这个模块中有多少可学习的参数是有益的。为了了解降维真正有帮助的地方,让我们假设前一层是前一个操作的输出,并且它的输出具有 256、28、28 的维度。现在让我们比较一下原始模块和图 4-2 中所示的降维模块。
天真模块:
-
8 核 1 × 1 卷积:2056 个参数 4 个
-
8 核 3 × 3 卷积:18440 个参数
-
8 核 5 × 5 卷积:51208 个参数
总共有 71704 个可学习参数。
降维模块:
-
8 核 1 × 1 卷积:2056 个参数
-
1 × 1 后跟 3 × 3 卷积:2640 个参数
-
1 × 1 后跟 5 × 5 卷积:3664 个参数
-
3 × 3 最大池,后跟 1 × 1 卷积:2056 个参数
总共有 10416 个可学习参数。对比一下可学习参数的数量,就能看出为什么说这个模块降维了。由于 1 × 1 卷积的智能放置,我们可以防止可学习参数的数量不受控制地激增。
一个初始网络简单地通过一个接一个地堆叠这些模块来构建。
多重成本函数:GoogLeNet
在图 4-3 中,你可以看到赢得imagenet挑战的 GoogLeNet 网络的主要结构。正如在开始引用的论文中所描述的,这个网络一个接一个地堆叠了几个初始模型。问题是,正如原始论文的作者很快发现的那样,中间层往往会“死亡”。这意味着他们在学习中不再扮演任何角色。为了让它们免于“死亡”,作者沿着网络引入了分类器,如图 4-3 所示。
网络的每个部分(图 4-3 中的部分 1、部分 2 和部分 3)将被训练为独立的分类器。这三个部分的训练不是独立发生的,而是同时发生的,与多任务学习中发生的非常相似(MTL)。
图 4-3
谷歌网络的高层架构
为了防止网络的中间部分变得不那么有效并逐渐消失,作者沿着网络引入了两个分类器,如图 4-3 中黄色方框所示。他们引入了两个中间损失函数,然后将总损失函数计算为辅助损失的加权和,有效地使用了通过以下公式评估的总损失:
Total Loss = Cost Function 1 + 0.3 * (Cost Function 2) + 0.3 * (Cost Function 3)
其中Cost Function 1是用第一部分评估的成本函数,Cost Function 2是用第二部分评估的,Cost Function 3是用第三部分评估的。测试表明,这是非常有效的,你会得到一个比简单地训练整个网络作为一个单一的分类器更好的结果。当然,辅助损失仅用于训练,而不用于推理。
作者开发了几个版本的初始网络,模块越来越复杂。如果你感兴趣,你应该阅读原文,因为它们很有教育意义。在 https://arxiv.org/pdf/1512.00567v3.pdf 可以找到作者的第二篇更复杂架构的论文。
Keras 中的初始模块示例
使用 Keras 的功能 API 使得构建一个初始模块变得非常容易。让我们看看必要的代码。出于空间原因,我们将不使用数据集构建完整的模型,因为这将占用太多空间,并且会分散对主要学习目标的注意力,即了解如何使用 Keras 构建一个具有并行评估而非顺序评估的图层的网络。
为了这个例子,让我们假设我们的训练数据集是CIFAR10。 5 这是用图像做的,都是 32 × 32 带三个通道(图像是彩色的)。因此,首先我们需要定义网络的输入层:
from keras.layers import Input
input_img = Input(shape = (32, 32, 3))
然后我们简单地定义一层接一层:
from keras.layers import Conv2D, MaxPooling2D
tower_1 = Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
tower_1 = Conv2D(64, (3,3), padding="same", activation='relu')(tower_1)
tower_2 = Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
tower_2 = Conv2D(64, (5,5), padding="same", activation="relu")(tower_2)
tower_3 = MaxPooling2D((3,3), strides=(1,1), padding="same")(input_img)
tower_3 = Conv2D(64, (1,1), padding="same", activation="relu")(tower_3)
这段代码将构建如图 4-4 所示的模块。Keras 功能 API 易于使用:您可以将层定义为另一层的功能。每个函数返回适当维数的张量。好的一面是你不用担心尺寸问题;你可以简单地定义一层又一层。只要注意使用正确的输入即可。例如,用这一行:
tower_1 = Conv2D(64, (1,1), padding="same", activation="relu")(input_img)
您定义了一个名为tower_1的张量,它是在使用input_img张量和 64 个 1 × 1 核进行卷积运算后计算的。然后这一行:
tower_1 = Conv2D(64, (3,3), padding="same", activation="relu")(tower_1)
定义一个新的张量,它是通过 64 个 3 × 3 核与前一行的输出进行卷积而获得的。我们取输入张量,与 64 个 1 × 1 核进行卷积,然后再次与 64 个 3 × 3 核进行卷积。
图 4-4
从给定代码构建的初始模块
层的连接很容易:
from keras.layers import concatenate
from tensorflow.keras import optimizers
output = concatenate([tower_1, tower_2, tower_3], axis = 3)
现在让我们添加几层文章:
from keras.layers import Flatten, Dense
output = Flatten()(output)
out = Dense(10, activation="softmax")(output)
然后我们最终创建模型:
from keras.models import Model
model = Model(inputs = input_img, outputs = out)
然后可以像往常一样编译和训练这个模型。用法的一个例子可能是
epochs = 50
model.compile(loss='categorical_crossentropy', optimizer=optimizers.Adam(), metrics=['accuracy'])
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, batch_size=32)
假设训练数据集由数组(X_train和y_train)组成,验证数据集由(X_test, y_test)组成。
注意
在 inception 模块的所有卷积运算中,您必须使用padding='same'选项,因为卷积运算的所有输出必须具有相同的维数。
本节简要介绍了如何使用 Keras 的功能 API 开发更复杂的网络架构。现在你应该对盗梦空间网络的工作原理和基本构件有了基本的了解。
题外话:喀拉斯的风俗损失
有时,在 Keras 中开发自定义损失是很有用的。来自官方的 Keras 文档( https://keras.io/losses/ ):
您可以传递现有损失函数的名称,也可以传递 TensorFlow/Theano 符号函数,该函数为每个数据点返回一个标量,并采用以下两个参数:
y_true:真标签。tensorlow/theano tensor。
y_pred:预言。与y_true形状相同的 TensorFlow/Theano 张量。
假设我们想要定义一个损失来计算预测的平均值。我们需要写这个
import keras.backend as K
def mean_predictions(y_true, y_pred):
return K.mean(y_pred)
然后我们可以简单地在编译调用中使用它,如下所示:
model.compile(optimizer='rmsprop',
loss=mean_predictions,
metrics=['accuracy'])
尽管这与其说是损失,不如说是有意义的。现在这开始变得有趣了,损失函数可以仅使用特定层的中间结果来评估。但要做到这一点,我们需要使用一个小技巧。因为根据官方文档,该函数只能接受真实的标签和预测作为输入。为此,我们需要创建一个函数,返回一个只接受真实标签和预测的函数。看起来很复杂。让我们看一个例子来理解它。假设我们有这个模型:
inputs = Input(shape=(512,))
x1 = Dense(128, activation=sigmoid)(inputs)
x2 = Dense(64, activation=sigmoid)(x1)
predictions = Dense(10, activation="softmax")(x2)
model = Model(inputs=inputs, outputs=predictions)
我们可以用这个代码 6 定义一个依赖于x1的损失函数(损失在做什么无关):
def custom_loss(layer):
def loss(y_true,y_pred):
return K.mean(K.square(y_pred - y_true) + K.square(layer), axis=-1)
return loss
那么我们可以像以前一样简单地使用损失函数:
model.compile(optimizer='adam',
loss=custom_loss(x1),
metrics=['accuracy'])
这是一种开发和使用定制损耗的简单方法。如初始网络中所述,有时能够训练具有多个损失的模型也是有用的。Keras 已经准备好了。定义损失函数后,可以使用以下语法
model.compile(loss = [loss1,loss2], loss_weights = [l1,l2], ...)
Keras 将用作损失函数
l1*loss1+l2*loss2
考虑到每个损失只会影响输入和损失函数之间路径上的权重。在图 4-5 中,你可以看到一个分成不同部分的网络:A、B和C。使用B的输出和C的loss2计算loss1。因此,loss1只会影响A和B中的权重,而loss2会影响A、B和C中的权重,如图 4-5 所示。
图 4-5
多个损失函数对不同网络部分影响的示意图
顺便提一下,这种技术在所谓的多任务学习 (MTL)中被大量使用。 7
如何使用预先训练的网络
Keras 提供预先训练的深度学习模型供您使用。这些被称为应用的模型可以用来预测新数据。这些模型已经在大数据集上进行了训练,因此不需要大数据集或长时间的训练。您可以在 https://keras.io/applications/ 的官方文档中找到所有申请信息。在撰写本文时,有 20 种型号可用,每一种都是以下型号之一的变体:
-
Xception
-
VGG16
-
VGG19
-
瑞斯网
-
ResNetV2
-
ResNeXt
-
不规则 3
-
InceptionResNetV2
-
MobileNet(移动网络)
-
MobileNetV2
-
DenseNEt
-
纳西网
让我们看一个例子,同时,让我们讨论函数中使用的不同参数。前期准备好的型号都在keras.applications包里。每个型号都有自己的包装。比如 ResNet50 在keras.applications.resnet50里。假设我们有一个想要分类的图像。我们可以使用 VGG16 网络,这是一个在图像识别方面非常成功的著名网络。我们可以从下面的代码开始
import tensorflow as tf
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.vgg16 import preprocess_input , decode_predictions
import numpy as np
然后我们可以简单地用一行代码加载模型
model = VGG16(weights='imagenet')
weights参数非常重要。如果权重为None,则权重被随机初始化。这意味着你得到了 VGG16 架构,你可以自己训练它。但是要知道,它大约有 1.38 亿个参数,所以你需要一个非常大的训练数据集和足够的耐心(以及非常强大的硬件)。如果您使用值imagenet,权重是通过使用imagenet数据集训练网络获得的。 8 如果你想要一个预先训练好的网络,你应该使用weights = 'imagenet'。
如果您在 Mac 上收到关于证书的错误信息,有一个简单的解决方案。上面的命令将尝试通过 SSL 下载权重,如果您刚刚从python.org安装了 Python,那么安装的证书将无法在您的机器上运行。只需打开一个 Finder 窗口,导航到Applications/Python 3.7(或者你已经安装的 Python 版本),双击Install Certificates.command。将会打开一个终端窗口,并运行一个脚本。之后,VGG16()调用将正常工作,不会出现错误消息。
之后,我们需要告诉 Keras 图像在哪里(假设您将它放在 Jupyter 笔记本所在的文件夹中)并加载它:
img_path = 'elephant.jpg'
img = image.load_img(img_path, target_size = (224, 224))
你可以在 GitHub 库的第四章的文件夹中找到这个图片。之后我们需要
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
首先,将图像转换为数组,然后需要扩展它的维度。意思如下:该模型处理成批图像,这意味着它将期望输入具有四个轴的张量(成批图像中的索引、沿 x 方向的分辨率、沿 y 方向的分辨率、通道数量)。但是我们的图像只有三个维度,水平和垂直分辨率以及通道的数量(在我们的例子中是三个,用于 RGB 通道)。我们需要为样本维度添加一个维度。更具体地说,我们的图像有维度(224,244,3),但模型期望一个维度(1,224,224,3)的张量,所以我们需要添加第一个维度。
这可以用 numpy 函数expand_dims()来完成,它只是在张量中插入一个新的轴。 9 作为最后一步,您需要预处理输入图像,因为每个模型期望与preprocess_input(x)调用略有不同的东西(在+1 和-1 之间,或者在 0 和 1 之间,等等)。
现在,我们准备让模型预测图像的类别,如下所示:
preds = model.predict(x)
要获得预测的前三类,我们可以使用decode_predictions()函数。
print('Predicted:', decode_predictions(preds, top=3)[0])
它将产生(我们的图像)以下预测:
Predicted: [('n02504013', 'Indian_elephant', 0.7278206), ('n02504458', 'African_elephant', 0.14308284), ('n01871265', 'tusker', 0.12798567)]
decode_predictions()以(class_name, class_description, score).的形式返回元组。第一个隐含的字符串是内部类名,第二个是描述(我们感兴趣的),最后一个是概率。根据 VGG16 网络,我们的图像似乎有 72.8%的可能性是印度大象。我不是大象方面的专家,但我会相信这个模型。要使用不同的预训练网络(例如 ResNet50),您需要更改以下导入:
from keras.applications.resnet50 import ResNet50
from keras.applications.resnet50 import preprocess_input, decode_predictions
你定义模型的方式:
model = ResNet50(weights='imagenet')
代码的其余部分保持不变。
迁移学习:导论
迁移学习是一种技术,在这种技术中,为解决特定问题而训练的模型被重新用于与第一个问题相关的新挑战。假设我们有一个多层网络。通常在图像识别中,第一层将学习检测一般特征,而最后一层将能够检测更具体的特征。 11 记住,在一个分类问题中,最后一层将有 N 个 softmax 神经元(假设我们正在分类 N 个类),因此必须学会针对你的问题非常具体。你可以通过下面的步骤直观地理解迁移学习,这里我们介绍一些我们将在接下来的章节中使用的符号。假设我们有一个有 n 层*L 层 的网络。*
- 我们在与我们的问题相关的大数据集(称为基础数据集*)上训练一个基础网络(或者得到一个预训练的模型)。例如,如果我们想要对狗的图像进行分类,我们可以在这个步骤中在
imagenet数据集上训练一个模型(因为我们基本上想要对图像进行分类)。重要的是,在这一步,数据集有足够的数据,并且任务与我们想要解决的问题相关。让一个网络接受语音识别训练将不会擅长狗的图像分类。这个网络可能不适合你的特定问题。*
** 我们得到一个新的数据集,我们称之为目标数据集(例如,狗的品种图像),这将是我们新的训练数据集。通常,该数据集将比步骤 1 中使用的数据集小得多。
* 然后你训练一个新的网络,在*目标数据集*上叫做*目标网络***。目标网络通常会有相同的第一个 *n* <sub>*k*</sub> (与*n*<sub>*k*</sub><*n*<sub>*L*</sub>)层我们的基础网络。前几层的可学习参数(假设 1 到 *n* <sub>*k*</sub> ,带*n*<sub>*k*</sub><*n*<sub>*L*</sub>)继承自步骤 1 中训练的基网络,在目标网络的训练过程中不改变。仅训练最后的和新的层(在我们的例子中从层 *n* <sub>*K*</sub> 到 *n* <sub>*L*</sub> )。其想法是,从 1 到 *n* <sub>*k*</sub> (来自基础网络)的层将在步骤 1 中学习足够的特征来区分狗和其他动物,并且 *n* <sub>*k*</sub> 到 *n* <sub>*L*</sub> (在目标网络中)的层将学习所需的特征有时,您甚至可以使用从基础网络继承的权重作为权重的初始值来训练整个目标网络,尽管这需要更强大的硬件。***
***### 注意
如果目标数据集很小,最佳策略是冻结从基础网络继承的图层,因为否则很容易使小数据集过拟合。
这背后的想法是,你希望在步骤 1 中,基本网络已经学会足够好地从图像中提取一般特征,因此你希望使用这种学到的知识,并避免再次学习的需要。但是,为了更好地进行预测,您需要针对具体情况对网络的预测进行微调,优化目标网络提取与问题相关的特定特征(通常发生在网络的最后一层)的方式。
换句话说,你可以这样想。要识别狗的品种,你必须遵循以下步骤:
-
你看着一张图片,决定它是不是一只狗。
-
如果你在观察一只狗,你把它分成几大类(例如,梗)。
-
之后,你把它们分成子类(例如,威尔士梗或西藏梗)。
迁移学习基于这样的想法,即步骤 1 和可能的步骤 2 可以从来自基本网络的大量通用图像(例如从imagenet数据集)中学习,并且步骤 3 可以在步骤 1 和步骤 2 中所学内容的帮助下通过小得多的数据集学习。
当目标数据集远小于基本数据集时,这是一个非常强大的工具,有助于避免训练数据集过拟合。
这种方法在用于预训练模型时非常有用。例如,使用在imagenet上训练的 VGG16 网络,然后仅重新训练最后几层通常是解决特定图像识别问题的极其有效的方式。您可以免费获得许多功能检测功能。请记住,在imagenet网络上训练这样的网络需要花费几千个 GPU 小时。对于没有必要的硬件和技术的研究人员来说,这通常是不可能的。在下一节中,我们将研究如何做到这一点。有了 Keras,这真的很容易,它将允许您解决图像分类问题的准确性,否则是不可能的。在图 4-6 中,你可以看到迁移学习过程的示意图。
图 4-6
迁移学习过程的示意图
狗和猫的问题
了解迁移学习在实践中如何工作的最好方法是在实践中尝试。我们的目标是能够尽可能地对狗和猫的图像进行分类,尽可能地用最少的努力(在计算资源上)。为了做到这一点,我们将使用狗和猫的图像数据集,你可以在 https://www.kaggle.com/c/dogs-vs-cats 的 Kaggle 上找到这些图像。警告:下载差不多 800MB。在图 4-7 中,你可以看到一些我们需要分类的图像。
图 4-7
包含在狗对猫数据集中的图像的随机样本
迁移学习的经典方法
解决这个问题的简单方法是创建一个 CNN 模型,并用图像训练它。首先,我们需要加载图像并调整它们的大小,以确保它们都具有相同的分辨率。如果检查数据集中的图像,您会注意到每个图像都有不同的分辨率。要做到这一点,让我们调整所有的图像到(150,150)像素。在 Python 中,我们会这样使用:
import glob
import numpy as np
import os
img_res = (150, 150)
train_files = glob.glob('training_data/*')
train_imgs = [img_to_array(load_img(img, target_size=img_res)) for img in train_files]
train_imgs = np.array(train_imgs)
train_labels = [fn.split('/')[1].split('.')[0].strip() for fn in train_files]
validation_files = glob.glob('validation_data/*')
validation_imgs = [img_to_array(load_img(img, target_size=img_res)) for img in validation_files]
validation_imgs = np.array(validation_imgs)
validation_labels = [fn.split('/')[1].split('.')[0].strip() for fn in validation_files]
假设我们在名为training_data的文件夹中有 3000 张训练图像,在名为validation_data的文件夹中有 1000 张验证图像,train_imgs和validation_imgs的形状如下:
(3000, 150, 150, 3)
(1000, 150, 150, 3)
像往常一样,我们将需要正常化的图像。现在每个像素的值在 0 到 255 之间,并且是一个整数。首先,我们将数字转换为浮点型,然后除以 255 进行归一化,这样每个值现在都在 0 和 1 之间。
train_imgs_scaled = train_imgs.astype('float32')
validation_imgs_scaled = validation_imgs.astype('float32')
train_imgs_scaled /= 255
validation_imgs_scaled /= 255
如果你检查train_labels,你会看到它们是字符串:'dog'或'cat'。我们需要将标签转换成整数,特别是 0 和 1。为此,我们可以使用名为LabelEncoder的 Keras 函数。
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
le.fit(train_labels)
train_labels_enc = le.transform(train_labels)
validation_labels_enc = le.transform(validation_labels)
我们可以用以下代码检查标签:
print(train_labels[10:15], train_labels_enc[10:15])
这将给出:
['cat', 'dog', 'cat', 'cat', 'dog'] [0 1 0 0 1]
现在我们已经准备好构建我们的模型了。我们可以通过下面的代码轻松做到这一点:
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout
from tensorflow.keras.models import Sequential
from tensorflow.keras import optimizers
model = Sequential()
model.add(Conv2D(16, kernel_size=(3, 3), activation="relu",
input_shape=input_shape))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, kernel_size=(3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(128, kernel_size=(3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(512, activation="relu"))
model.add(Dense(1, activation="sigmoid"))
model.compile(loss='binary_crossentropy',
optimizer=optimizers.RMSprop(),
metrics=['accuracy'])
这是一个小型网络,其结构如下:
Layer (type) Output Shape Param #
==============================================================
conv2d_3 (Conv2D) (None, 148, 148, 16) 448
______________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 74, 74, 16) 0
______________________________________________________________
conv2d_4 (Conv2D) (None, 72, 72, 64) 9280
______________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 36, 36, 64) 0
______________________________________________________________
conv2d_5 (Conv2D) (None, 34, 34, 128) 73856
______________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 17, 17, 128) 0
______________________________________________________________
flatten_1 (Flatten) (None, 36992) 0
______________________________________________________________
dense_2 (Dense) (None, 512) 18940416
______________________________________________________________
dense_3 (Dense) (None, 1) 513
==============================================================
Total params: 19,024,513
Trainable params: 19,024,513
Non-trainable params: 0
______________________________________________________________
在图 4-8 中,您可以看到网络的示意图,以便了解层序列。
图 4-8
网络的示意图,让您了解层序列
此时,我们可以使用以下内容训练网络:
batch_size = 30
num_classes = 2
epochs = 2
input_shape = (150, 150, 3)
model.fit(x=train_imgs_scaled, y=train_labels_enc,
validation_data=(validation_imgs_scaled, validation_labels_enc),
batch_size=batch_size,
epochs=epochs,
verbose=1)
通过两个时期,我们达到了大约 69%的验证准确率和 70%的训练准确率。不是一个好结果。让我们看看我们能否在短短两个时代内做得比这更好。在两个时期内这样做的原因仅仅是为了快速检查不同的可能性。对这样的网络进行多次训练只需几个小时。请注意,该模型过度拟合了训练数据。当训练更多的纪元时,这变得清晰可见,但这里的主要目标不是获得最佳模型,而是看看如何使用预训练的模型来获得更好的结果,所以我们将忽略这个问题。
现在我们来导入 VGG16 预训练网络。
from tensorflow.keras.applications import vgg16
from tensorflow.keras.models import Model
import tensorflow.keras as keras
base_model=vgg16.VGG16(include_top=False, weights="imagenet")
请注意,include_top=False参数删除了网络的最后三个完全连接的层。这样,我们可以将自己的层附加到基本网络中,代码如下:
from tensorflow.keras.layers import Dense,GlobalAveragePooling2D
x=base_model.output
x=GlobalAveragePooling2D()(x)
x=Dense(1024,activation='relu')(x)
preds=Dense(1,activation='softmax')(x)
model=Model(inputs=base_model.input,outputs=preds)
我们添加了一个 pooling 层,然后是一个有 1024 个神经元的Dense层,然后是一个有一个神经元的输出层,这个神经元有一个 softmax 激活函数,做二分类。我们可以使用以下内容检查结构:
model.summary()
输出很长,但是最后你会发现:
Total params: 15,242,050
Trainable params: 15,242,050
Non-trainable params: 0
目前所有的 22 层都是可训练的。为了能够真正进行迁移学习,我们需要冻结 VGG16 基础网络的所有层。为此,我们可以做到以下几点:
for layer in model.layers[:20]:
layer.trainable=False
for layer in model.layers[20:]:
layer.trainable=True
该代码将前 20 层设置为不可训练状态,后两层设置为可训练状态。然后我们可以如下编译我们的模型:
model.compile(optimizer='Adam',loss='sparse_categorical_crossentropy',metrics=['accuracy'])
注意,我们使用了loss='sparse_categorical_crossentropy'来使用标签,而不必对它们进行热编码。正如我们之前所做的,我们现在可以训练网络:
model.fit(x=train_imgs_scaled, y=train_labels_enc,
validation_data=(validation_imgs_scaled, validation_labels_enc),
batch_size=batch_size,
epochs=epochs,
verbose=1)
请注意,虽然我们只训练了网络的一部分,但这将比我们之前尝试的简单网络需要更多的时间。结果将是两个时期内惊人的 88%。比以前好得多的结果!您的输出应该如下所示:
Train on 3000 samples, validate on 1000 samples
Epoch 1/2
3000/3000 [==============================] - 283s 94ms/sample - loss: 0.3563 - acc: 0.8353 - val_loss: 0.2892 - val_acc: 0.8740
Epoch 2/2
3000/3000 [==============================] - 276s 92ms/sample - loss: 0.2913 - acc: 0.8730 - val_loss: 0.2699 - val_acc: 0.8820
这要归功于预先训练好的第一层,这为我们节省了很多工作。
迁移学习实验
如果我们想为目标网络尝试不同的体系结构,并且想再增加几层并重试,该怎么办?前一种方法有一个小小的缺点:我们需要在每次事件中训练整个网络,尽管只需要训练最后几层。从上一节可以看出,一个时期大约需要 4.5 分钟。我们能更有效率吗?事实证明我们可以。
考虑图 4-9 中描述的配置。
图 4-9
实践中一种更灵活的迁移学习方式的示意图
我们的想法是生成一个新的数据集,我们称之为带有冻结图层的特征数据集 、 。由于它们不会因训练而改变,这些层将总是生成相同的输出。我们可以使用这个特征数据集作为一个小得多的网络(我们称之为目标子网)的新输入,该网络仅由我们在上一节中添加到基础层的新层构成。我们只需要训练几层,这样会快很多。生成特征数据集将需要一些时间,但这必须只进行一次。此时,您可以为目标子网测试不同的架构,并为您的问题找到最佳配置。让我们看看如何在 Keras 做到这一点。基础数据集准备与之前相同,因此我们不再重复。
让我们像以前一样导入 VGG16 预训练网络:
from tensorflow.keras.applications import vgg16
from tensorflow.keras.models import Model
import tensorflow.keras as keras
vgg = vgg16.VGG16(include_top=False, weights="imagenet",
input_shape=input_shape)
output = vgg.layers[-1].output
output = keras.layers.Flatten()(output)
vgg_model = Model(vgg.input, output)
vgg_model.trainable = False
for layer in vgg_model.layers:
layer.trainable = False
其中input_shape为(150, 150, 3)。
我们可以简单地用几行代码生成features数据集(使用predict功能):
def get_ features(model, input_imgs):
features = model.predict(input_imgs, verbose=0)
return features
train_features_vgg = get_features(vgg_model, train_imgs_scaled)
validation_features_vgg = get_features(vgg_model, validation_imgs_scaled)
请注意,在现代笔记本电脑上,这将需要几分钟时间。在现代的 MacBook Pro 上,这将需要 40 分钟的 CPU 时间,这意味着如果你有更多的核心/线程,它将占用其中的一小部分。在我的笔记本电脑上,只需要 4 分钟。请记住,由于我们使用了参数include_top = False,网络末端的三个dense层已经被移除。train_features_vgg将只包含基本网络最后一层的输出,而没有最后三个dense层。此时,我们可以简单地构建我们的目标子网:
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, InputLayer
from tensorflow.keras.models import Sequential
from tensorflow.keras import optimizers
input_shape = vgg_model.output_shape[1]
model = Sequential()
model.add(InputLayer(input_shape=(input_shape,)))
model.add(Dense(512, activation="relu", input_dim=input_shape))
model.add(Dropout(0.3))
model.add(Dense(512, activation="relu"))
model.add(Dropout(0.3))
model.add(Dense(1, activation="sigmoid"))
model.compile(loss='binary_crossentropy',
optimizer=optimizers.Adam(lr =1e-4),
metrics=['accuracy'])
model.summary()
训练这个网络会比以前快很多。您将在几秒钟内获得 90%的准确率(请记住,这次您已经创建了一个新的训练数据集)。但是现在你可以改变这个网络,测试不同的架构会快得多。这一次,一个历元只需要 6 秒钟,而前一个例子需要 4.5 分钟。这个方法比前一个效率高得多。我们将培训分为两个阶段:
-
创建特征数据集。只做过一次。(在我们的示例中,这需要大约四分钟。)
-
使用特征数据集作为输入,将新图层训练为独立网络。(每个时期需要 6 秒钟。)
如果我们想训练我们的网络 100 个纪元,用这种方法我们需要 14 分钟。使用上一节描述的方法,我们将需要 7.5 小时!缺点是您需要为每个想要使用的数据集创建新的特征数据集。在我们的例子中,我们需要为训练和验证数据集这样做。
Footnotes 1原文可以在 arXiv 档案上通过以下链接获得: http://toe.lt/4 。
2
通过计算预算,我们可以确定执行特定计算(例如,训练网络)所需的时间和硬件资源。
3
记住在这种情况下,我们有一个权重和一个偏差。
4
记住在这种情况下,我们有一个权重和一个偏差。
5
您可以在 https://www.cs.toronto.edu/~kriz/cifar.html 找到数据集的所有信息。
6
代码的灵感来自 http://toe.lt/7 。
7
你可以在 https://en.wikipedia.org/wiki/Multi-task_learning 找到更多信息
8
9
您可以在 http://toe.lt/5 查看该功能的官方文档。
10
这个术语已经被洋辛基用在了 https://arxiv.org/abs/1411.1792 中。
11
你可以在 https://arxiv.org/abs/1411.1792 找到 Yosinki 等人关于这个主题的一篇非常有趣的论文。
***
五、成本函数和风格迁移
在这一章中,我们将更深入地研究成本函数在神经网络模型中的作用。特别是,我们将讨论 MSE(均方误差)和交叉熵,并讨论它们的来源和解释。我们将着眼于为什么我们可以使用它们来解决问题,MSE 如何在统计意义上解释,以及交叉熵如何与信息论相关。然后,给你一个更高级的特殊损失函数的例子,我们将学习如何进行神经风格迁移,这里我们将讨论一个神经网络以著名画家的风格绘画。
神经网络模型的组件
至此,您已经看到并开发了几个试图解决不同类型问题的模型。你现在应该知道,在所有的神经网络模型中,(至少)有三个主要构件:
-
网络架构(层数、层类型、激活功能等。)
-
损失函数(MSE,交叉熵等。)
-
优化器
优化器通常不是特定于问题的。例如,为了解决回归或分类问题,您需要选择不同的体系结构和损失函数,但是在这两种情况下您可以使用相同的优化器。在回归中,您可以使用前馈网络和 MSE 作为损失函数。在分类中,你可以选择卷积神经网络和交叉熵损失函数。但是在这两种情况下,您都可以使用 Adam 优化器。在决定网络可以学习什么方面起最大作用的组件是损失函数。改变它,你就会改变你的网络能够预测和学习的东西。
培训被视为一个优化问题
让我们试着更详细地理解为什么会这样。从纯理论的角度来看,训练一个网络无非就是解决一个真正复杂的优化问题。连续优化问题的标准公式是寻找给定函数的最小值
受制于两种约束类型
其中f:ℝn→ℝ是我们要最小化的连续函数,gI(x)≤0 表示不等式约束,pj(x)= 0 表示等式约束, m 当然,没有约束也有可能出现问题。但是这和神经网络有什么关系呢?可以得出以下相似之处:
-
函数 f ( x )是我们在建立神经网络模型时选择的损失函数。
-
输入 x ∈
ℝn 是我们网络的权值(可学习参数)。请记住,我们可能选择的任何损失函数总是网络输出的函数(我们用表示),并且输出总是权重 W (网络的可学习参数)的函数。
当我们训练一个网络时,我们实际上是在解决一个优化问题,一个我们想要最小化关于权重的损失函数的问题。我们隐式地拥有约束,尽管我们通常不会显式地声明它们。例如,我们可能有这样的约束,即我们希望一次观察所需的推断时间少于 10ms。在这种情况下,我们将有 n = 0(没有等式约束), m = 1(一个不等式约束),其中 g 1 是推理运行时间。引用维基百科 1 :
损失函数或成本函数是将一个事件或一个或多个变量的值映射到一个实数上的函数,该实数直观地表示与事件相关的一些“成本”
通常,损失函数衡量模型对数据的理解程度。让我们来看几个简单的例子,这样你就可以在一个具体的案例中理解网络训练的这个公式。
一个具体的例子:线性回归
如你所知,如果你选择身份函数 2 作为激活函数,你可以用一个只有一个神经元的网络进行线性回归。我们用x[I]∈ℝn和 i = 1、…, m 来表示观察值集合,其中 m 是我们拥有的观察值的数量。神经元(以及网络)将会有输出
这里我们用 w = ( w 1 ,… w n )来表示权重。我们可以选择损失函数作为均方误差(MSE):
其中 y [ i ] 是我们要为第 i th 观察预测的目标变量。很容易看出,我们定义的损失函数是权重和偏差的函数。事实上,我们有
像我们通常使用(例如)梯度下降算法那样训练该网络无非是解决一个无约束优化问题,其中我们有(使用我们在开始时使用的符号):
成本函数
数学符号
让我们定义一些我们将在下一节中使用的符号。我们将使用
是对 i * th * 观察的网络输出。
是包含所有观测值的网络输出的张量。3
代表 i * th 观察输入特性(一般来说,对于图像我们会有 n c 通道,以及分辨率为n*x×ny)。
是包含所有输入观测值的张量。
w 是网络中使用的所有可学习参数的集合(包括偏差)。
m 是观察次数。
n c 是图像通道的数量(对于 RGB 图像为 3)。
n x 是输入图像的水平分辨率。
ny是输入图像的垂直分辨率。
J 是成本函数。
通常,我们将所谓的成本(或损失)函数 J 一般定义如下:
除了网络架构之外,该功能将定义我们的神经网络模型能够解决什么样的问题。注意这个函数是如何
-
取决于网络架构,因为它取决于网络输出
(并因此取决于可学习的参数 W)
-
取决于输入数据集,因为它取决于输入 X
这是寻找最佳权重时将使用的函数。在几乎所有的优化器中,权重都以某种形式使用来更新。
典型成本函数
正如我们在前面章节中看到的,在训练神经网络时,可以使用几个成本函数。在接下来的章节中,我们将详细介绍两个最常用的词汇,并试图理解它们的含义和来源。
均方误差
均方误差函数
可能是开发回归模型时最常用的成本函数。对于这个成本函数有几种解释,但是下面两种应该可以帮助你对它有一个直观和更正式的理解。
直观的解释
J 无非是预测值和实测值的平方差的平均值。所以基本上,它衡量的是预测值与期望值的差距。一个能够完美预测数据的完美模型(代表所有的 i = 1,…, m )应该是 J = 0。一般来说,它保持最小的 J 预测越好。
注意
一般来说,它认为 MSE 越小,预测越好(因此,模型越好)。
最小化 MSE 意味着找到参数,使我们的网络输出尽可能接近我们的训练数据。请注意,您可以通过使用以下公式给出的 MAE(平均绝对误差)来获得类似的结果
尽管通常不这样做。
MSE 作为矩生成函数的二阶矩
有一种更正式的方法来解释 MSE。让我们定义数量
让我们定义力矩生成函数
这里我们有 t ∈ ℝ,我们用 E [ ]表示变量在所有观测中的期望值。我们将跳过关于期望值存在与否的讨论,取决于δY的特性,因为这超出了本书的范围。我们可以用泰勒级数展开法来展开etδY(我们假设我们可以这样做):
因此
E【δYn称为函数的 n th 矩MδY(t)。你可以看到,这些时刻很容易解释(至少第一次):
- E**δY:MδY(t)-δY 的一阶矩
** E[δY2:M*δY(t)-就是我们定义的 MSE 函数*
** *E*[*δY*<sup>3</sup>:M*<sub>*δY*</sub>(*t*)-*偏斜度* <sup>[5</sup>*
** *E**δY*<sup>4</sup>:M*<sub>*δY*</sub>(*t*)-*峰度* <sup>[6</sup>****
**我们可以简单地把二阶矩写成观测值的平均值
如果我们假设我们的模型用E【δY】= 0 来预测数据,那么E【δY2(因此 MSE)无非是我们的数据点分布的方差δY[I。在这种情况下,它只是测量我们的点在平均值(即零)周围的分布范围:完美的预测。记住,如果对于一个观测,我们有δY[I]= 0,这意味着我们有,意味着预测是完美的。只是为了给出正确的术语,如果E[δY]不为零,那么这些矩有时被称为非中心矩 。如果你正在处理非中心矩,你不能再直接把它们解释为统计量(方差)。
注意
如果你正在处理非中心矩,你不能再直接把它们解释为统计量(方差)。如果δY[I]的平均值为零,那么 MSE 就是我们预测的分布的方差。当然,值越小,预测就越准确。
交叉熵
有几种理解交叉熵损失函数的方法,但我认为最迷人的方法是从信息论开始讨论。在这一节中,我们将在更直观的基础上讨论一些基本概念,以给你足够的信息和理解,从而对交叉熵有一个非常有力的理解。
事件的自我信息或抑制
我们需要从自我信息的概念开始,或者说一个事件的极限。为了对它有一个直观的理解,请考虑以下几点:当一个事件发生一个不可能的结果时,我们把它与高层次的信息联系起来。当一个结果总是发生时,通常它没有太多的相关信息。换句话说,当不太可能的事件发生时,我们会更惊讶;因此,它也被称为一个结果的上限。我们如何用数学的形式来表达它呢?我们来考虑一个随机变量 X 带 n 可能结果 x 1 , x 2 ,…, x n 和概率质量函数7P(X)。让我们用*I=P(xI)来表示事件xI发生的概率。在 0 和 1 之间的任何单调递减函数I*(pI)都可以用来表示随机变量 X 的上界(或自身信息)。但是这个函数必须有一个重要的性质:如果事件是独立的,那么 I 应该满足**
*如果结果 i 和 j 是独立的。人们马上想到一个具有这种特性的函数:对数。事实上,这是事实
为了让它单调递减,我们可以选择以下公式:
与事件 X 相关的 Suprisal
总的来说,我们有多少关于特定事件的信息?这是通过对 X 的所有可能结果的期望值来衡量的(我们将用 P 来表示这个集合)。数学上,我们可以把它写成
H ( X )被称为香农熵,而 b 是算法的基础,通常被选为 2、10 或 e 。
交叉熵
现在让我们假设我们想要比较事件 X 的两种概率分布。我们来分析一下,当我们训练一个神经网络进行分类时,我们做了什么。请考虑以下几点:
-
我们的例子给出了事件的“真实”或预期分布(真实标签)。他们的分布将是我们的 P。例如,我们的观测可能包含具有一定概率的猫类(假设这是类 1)P(x1,其中x1 是结果“这个图像中有一只猫”。我们有给定的概率质量函数, P 。
-
我们训练的网络将会给我们一个不同的概率质量函数, Q ,因为预测将不会与训练数据完全相同。结局x1(“图像里有一只猫”)会以不同的概率发生, Q ( x 1 )。您应该记得,在构建分类网络时,我们对输出层使用了一个
softmax激活函数来将输出解释为概率。你看到所有的事情突然变得更有意义了吗?
我们希望有一个尽可能反映给定标签的预测,这意味着我们希望有一个尽可能类似于 P 的概率质量函数 Q 。
为了比较两个概率质量函数(我们感兴趣的),我们可以简单地用实例得到的分布来计算我们的网络得到的自我信息的期望值。以更数学的形式
如果你有信息论方面的经验, H ( Q , P )会给出两个概率质量函数 Q 和 P 相似性的度量。为了理解为什么,让我们考虑一个实际的例子。这将是一场公平的掷硬币游戏。 X 将有两种可能的结果:X1 将是硬币的头部,而X2 将是硬币的尾部。“真实的”概率质量函数当然是一个常数函数,其中P(x1)= 0.5,P(x2)= 0.5。现在让我们考虑另一种概率质量函数 Q i (为了说明的目的,我们将只考虑 9 个可能的值):
-
I= 1→q1(x1= 0.1,q
-
I= 2→q2(x??= 0.2,q
-
I= 3→【q】()= 0.3,【q11】
** I= 4→q4(x1= 0.4,q
* *I*= 5→q<sub>5</sub>(*x*<sub>1</sub>= 0.5,*q*
* *I*= 6→q6(*x*??= 0.6,*q*
* *I*= 7→q<sub>7</sub>(*x*<sub>1</sub>= 0.7,*q*
* *I*= 8→q8(*x*1= 0.8,*q*
* *I*= 9→q<sub>9</sub>(*x*<sub>1</sub>= 0.9,*q**
我们来计算一下H*(QI, P )对于 i = 1,…5。对于 i = 6,..我们不需要计算 H 。。,9 既然函数是对称的,意思就是比如说那个H(Q4,P)=H(Q6, P )。在图 5-1 中可以看到H(QI, P )的剧情。你可以看到当两个概率质量函数相同时,当 i = 5 时达到最大值。
图 5-1
H(Q i ,P)为 i = 1,…5。当两个概率质量函数完全相同时,对于 i = 5 获得最小值。
注意
交叉熵 H ( Q , P )是两个质量概率函数 Q 和 P 相似程度的度量。
二元分类的交叉熵
现在让我们考虑一个二元分类问题,看看交叉熵是如何工作的。假设我们的事件 X 是给定图像的两类分类。可能的结果只有两个:1 类或 2 类。为了说明的目的,让我们假设我们的图像属于类别 1。我们对于图像的“真实”概率质量函数将具有P(x1)= 1.0,P(x2)= 0。换句话说,由于我们知道真实值,我们的概率质量函数 P 只能是 0 或 1。
你会记得,在一个二元分类问题中,我们使用了以下内容
其中 y ( j ) 表示真实标签(0 表示类 1,1 表示类 2),而是图像 j 属于类 2 的概率,或者换句话说,是假设值为 1 的网络输出的概率。我们将最小化的成本函数由所有观察值(或例子)的总和给出
使用上一节的符号,我们可以为图像 j 编写
记住y??(j)只能是 0 或 1;所以我们只有两种可能:pj(x1)= 1,pj(x2)= 0 或者pj(x 我们也可以写网络的预测
请记住:这个结果是由我们如何构建我们的网络(因为我们在输出层使用了softmax激活函数来获得概率)和我们如何编码我们的标签(0 和 1,以便它们可以被解释为概率)决定的。现在,让我们使用我们的神经网络符号来编写上一节中定义的交叉熵,但是对所有示例求和(请记住,我们希望获得所有事件的整个交叉熵,换句话说,所有图像的交叉熵):
所以基本上只不过是在信息论中得到的交叉熵。
注意
直觉上,当我们最小化二元分类问题中的交叉熵时,我们最小化了当我们的预测与我们的期望不同时我们可能有的惊讶。
H ( Q , P )衡量我们的预测概率密度函数( Q )与我们的训练样本概率密度函数( P )的匹配程度。
注意
当我们使用交叉熵设计用于分类的网络,并且我们在最终层使用softmax激活函数来将输出解释为概率时,我们简单地构建了基于信息论的复杂分类系统。我们应该感谢香农 8 用神经网络进行分类。
成本函数:最后一句话
现在应该很清楚,成本函数决定了神经网络可以学习什么。改变它,网络就会学到完全不同的东西。毫不奇怪,要获得特殊的结果,比如艺术,只需要选择正确的架构和正确的成本函数。在本章的下一部分,我们将着眼于神经类型转移,选择正确的成本函数(在这个例子中,我们将看到多个)是实现非凡结果的关键,这一点将变得非常清楚。
神经类型转移
此时,您已经拥有了开始使用网络进行更高级技术的所有工具:使用预先训练的 CNN,从隐藏层提取信息,以及使用自定义成本函数。这开始成为高级材料,所以你需要很好地理解我们在前面章节中讨论的所有基础知识。如果有什么不清楚的地方,回头再研究一遍。
CNN 的一个有趣而好玩的应用是制作艺术品,神经风格迁移(NST)指的是一种操纵数字图像的技术,采用另一幅图像的外观或风格 9 。一个有趣的应用程序是拍摄一幅图像,让网络操纵它,使其采用著名画家的风格,比如梵高。使用深度学习的 NST 最早出现在 Gatys 等人 2015 年的一篇论文中 10 。这是一种新技术。Gatys 开发的方法使用预先训练的深度 CNN 来将图像的内容与风格分开。
这个想法是将一幅图像输入预先训练好的 VGG-19 11 CNN,在imagenet数据集上进行训练。作者假设图像的内容可以在网络中间层输出中找到(图像通过每层中的学习过滤器),而风格在于不同层输出的相关性(编码在格拉米矩阵中)。预先训练的网络可以很好地识别图像的内容,因此每一层学习的特征必须与图像的内容紧密相关,而不是与风格相关。事实上,一个擅长识别图像的健壮的 CNN 并不太在乎风格。直观地说,风格包含在图像空间上不同的滤波器响应是如何相关的。画家可能会使用宽或窄的笔触,可能会使用许多彼此接近的颜色或仅使用几种颜色,等等。请记住,在 CNN 中,每一层都只是图像过滤器的集合;因此,给定层的输出只是输入图像的不同过滤版本的集合 10。
另一种方式是,当你从远处看一幅图像时(你不太关心细节),内容被发现,而当你在更近的尺度上看图像时,风格被发现,这取决于图像的不同部分如何相互联系。Gatys 等人聪明地用数学方法简单地实现了这些想法。为了给你一个思路,请看图 5-2 。一个网络已经将原始图像(左上)处理成了右上角梵高画作的风格,以获得底部的图像。
图 5-2
NST 的一个例子。该方法将原始图像(左上)处理成右上的梵高绘画风格,以获得底部的图像。
NST 背后的数学
原始论文使用的是 VGG19 网络,Keras 提供给我们下载和使用。我们在这里用 x 表示的输入图像(我将尽可能使用原始符号)被编码在 CNN 的每一层中。带有Nl过滤器(有时也称为内核)的图层将具有 N l 特征地图作为输出。在该算法中,这些输出将在尺寸为 M l 的一维向量中展平,其中 M l 是当应用于输入图像时每个滤波器的输出的高度乘以宽度。层 l 的响应可以被编码到张量中。让我们在这里暂停一下,试着用一个具体的例子来理解我们的意思。
假设我们使用彩色图像作为输入图像,每个图像的尺寸为 32 × 32。让我们考虑用代码创建的 CNN 中的第一个卷积层:
Conv2D(32, (3, 3), padding="same", activation="relu", input_shape=input_shape))
当然是哪里input_shape = (32,32,3)。图层的输出将具有以下尺寸
(None, 32, 32, 32)
其中当然None将假设所使用的观察值的数量。这是因为我们使用了参数padding = 'same'。在这种情况下,层 l = 1 的输出是 32 个特征图(或输入图像与 32 个过滤器卷积的结果),每个尺寸为 32 × 32。在这种情况下,我们将有 N l = 1 = 32 和Ml= 1= 32×32 = 1024。在计算格拉米矩阵之前,将展平每个 32 × 32 的特征图。您将在后面的代码中清楚地看到这是如何实现的。
我们姑且称原图 p 。这就是我们想要改变的形象。作为输出生成的图像被称为 x 。我们将用 P l 和 F l 表示它们各自从图层 l 中得到的特征图。我们定义称为内容损失函数的平方误差损失如下:
在 Keras 中,我们将使用以下代码实现这一点:
content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2)
for name in content_outputs.keys()])
其中content_outputs[]和content_targets[]将分别包含应用于输入(content_outputs)和生成的图像(content_targets)时 VGG19 的特定层的输出(已经展平)。稍后我们将更详细地讨论它;如果你没有完全理解,暂时不要担心。你可能想知道为什么我们没有因子 1/2,但我们并不需要它,因为将乘以另一个因子,这将使 1/2 无用。
我们需要计算损失函数相对于图像的梯度。这是相当重要的一点。这意味着我们想要学习的参数是我们想要改变的图像的像素值。网络的参数是固定的,我们不需要改变它们。对于 Keras,我们需要使用以下形式的tape.gradient函数:
tape.gradient(loss, image)
我们需要将图像定义为一个 TensorFlowVariable(稍后会详细介绍)。如果你不熟悉tape.gradient的工作原理,我建议你去 https://www.tensorflow.org/tutorials/eager/automatic_differentiation 查阅官方文档。
注意
我们要学习的参数是我们要改变的图像的像素值,而不是网络的权重。
现在我们需要注意风格。为此,我们需要为样式定义一个损失函数。为此,我们需要定义 Gramian 矩阵Gl,它是图层 l 中展平后的特征图 i 和 j 之间的内积。换句话说
有了这个新定义的量,我们将定义一个样式损失函数,其中 a 是我们想要使用样式的图像
在哪里
其中 w l 为原试卷中选取的权重,等于 1/5。在 Keras 中,我们将通过代码实现这种丢失(我们将在后面查看细节):
tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2)
for name in style_outputs.keys()])
style_outputs和style_targets变量将包含 VGG19 网络五层的输出。在原始论文中,使用了以下五层:
l=1 - block1_conv1
l=2 - block2_conv1
l=3 - block3_conv1
l=4 - block4_conv1
l=5 - block5_conv1
这些是 VGG19 网络中每个模块的第一层。请记住,您可以通过以下代码从 VGG19 中获取图层名称:
vgg = tf.keras.applications.VGG19(include_top=False, weights="imagenet")
print()
for layer in vgg.layers:
print(layer.name)
会得到这样的结果:
input_1
block1_conv1
block1_conv2
block1_pool
block2_conv1
block2_conv2
block2_pool
block3_conv1
block3_conv2
block3_conv3
block3_conv4
block3_pool
block4_conv1
block4_conv2
block4_conv3
block4_conv4
block4_pool
block5_conv1
block5_conv2
block5_conv3
block5_conv4
block5_pool
注意,我们没有密集层,因为我们使用了include_top=False。最后,我们将最小化下面的损失函数
使用梯度下降(例如),相对于我们想要改变的图像。可以选择常数 α 和 β 来赋予样式或内容更大的权重。对于图 5-1 中的结果,我选择了 α = 1.0, β = 10 4 。其他典型值有α= 10—2, β = 10 4 。
Keras 风格迁移的一个例子
我们将在此讨论的代码取自最初的 TensorFlow NST 教程,并针对本次讨论进行了极大的简化。为了简化讨论,我们将只讨论部分代码,因为整个代码相对较长。你可以在这本书的 GitHub 资源库的 Chapter 5 文件夹中找到完整的简化版。我建议你在启用 GPU 的情况下运行 Google Colab 中的代码,因为它的计算量相当大。给你一个概念,在我的笔记本电脑上,一个 epoch 大约需要 13 秒,而在谷歌 Colab 上,处理 512 × 512 像素的图像需要 0.5 秒。
为了确保您安装了最新的 TensorFlow 版本,您应该在笔记本的开头运行以下代码:
from __future__ import absolute_import, division, print_function, unicode_literals
!pip install tensorflow-gpu==2.0.0-alpha0
import tensorflow as tf
如果您在 Google Colab 上运行代码,您需要将想要处理的图像保存在 Google drive 上并挂载它。为此,您需要在硬盘上上传两个图像:
-
一个风格形象:比如一幅名画。这是您想要从中获取样式的图像。
-
内容图像:例如,您拍摄的风景或照片。这是您要修改的图像。
我在这里假设你已经把你的图片上传到了 Google drive 根目录下的一个名为data的文件夹中。你现在需要做的是在 Google Colab 中安装你的 Google drive 来访问这些图片。为此,您需要以下代码:
from google.colab import drive
drive.mount('/content/drive')
如果您运行这段代码,您需要转到一个特定的 URL(由 Google Colab 提供),在那里您将收到需要粘贴到笔记本中的代码。在 http://toe.lt/a 可以找到关于如何做到这一点的很好的概述。装载后,您将获得目录中的文件列表,如下所示:
!ls "/content/drive/My Drive/data"
我们可以定义我们将使用的图像的文件名
content_path = '/content/drive/My Drive/data/landscape.jpg'
style_path = '/content/drive/My Drive/data/vangogh_landscape.jpg'
当然,您需要将文件名改为您自己的文件名。但是如果您想尝试使用这些图片,您可以在 GitHub 存储库中找到我在这个例子中使用的图片。如果没有的话,您需要创建data目录,并将图像复制到那里。图像将通过load_img()功能加载。请注意,在开头的函数中,我们调整了图像的大小,使其最大尺寸等于 512(load_img()函数的完整代码可以在 GitHub 上找到)。这是一个可管理的大小,但是如果您想生成更好看的图像,您需要增加这个值。图 5-1 中的图像是用max_dim = 1024生成的。该函数开始于
def load_img(path_to_img):
max_dim = 512
img = tf.io.read_file(path_to_img)
因此,您更改了max_dim变量的值来处理更大的图像。现在,我们只需要选择一些层的输出,正如我们在上一节中所描述的。为此,我们将想要使用的层的名称放在两个列表中:
# Content layer where will pull our feature maps
content_layers = ['block5_conv2']
# Style layer we are interested in
style_layers = ['block1_conv1',
'block2_conv1',
'block3_conv1',
'block4_conv1',
'block5_conv1']
这样,我们可以使用名称选择正确的层。我们需要的是一个模型,从每一层获取输入并返回所有的特征地图。为此,我们使用以下代码
def vgg_layers(layer_names):
vgg = tf.keras.applications.VGG19(include_top=False, weights="imagenet")
vgg.trainable = False
outputs = [vgg.get_layer(name).output for name in layer_names]
model = tf.keras.Model([vgg.input], outputs)
return model
此函数获取一个带有图层名称的列表作为输入,并使用以下代码行选择给定图层的网络层输出:
outputs = [vgg.get_layer(name).output for name in layer_names]
请注意,没有检查,所以如果你有一个错误的层名称,你不会得到你期望的结果。但是由于我们需要的层是固定的,所以不需要检查网络中是否存在这些名称。这条线
model = tf.keras.Model([vgg.input], outputs)
根据layer_names输入列表中的层数,创建一个有一个输入(vgg.input)和一个或多个输出的模型。
为了计算(格拉米矩阵),我们使用这个函数
def gram_matrix(input_tensor):
result = tf.linalg.einsum('bijc,bijd->bcd', input_tensor, input_tensor)
input_shape = tf.shape(input_tensor)
num_locations = tf.cast(input_shape[1]*input_shape[2], tf.float32)
return result/(num_locations)
其中变量num_locations简单来说就是 M l 。现在有趣的部分来了:损失函数的定义。我们需要定义一个名为StyleContentModel的类,它将接受我们的模型,并在每次迭代中返回不同层的输出。该类有一个__init__部分,我们将在这里跳过(您可以在 Jupyter 笔记本中找到代码)。有趣的部分是call()功能:
def call(self, inputs):
inputs = inputs*255.0
preprocessed_input = tf.keras.applications.vgg19.preprocess_input(inputs)
outputs = self.vgg(preprocessed_input)
style_outputs, content_outputs = (outputs[:self.num_style_layers],
outputs[self.num_style_layers:])
style_outputs = [gram_matrix(style_output)
for style_output in style_outputs]
content_dict = {content_name:value
for content_name, value
in zip(self.content_layers, content_outputs)}
style_dict = {style_name:value
for style_name, value
in zip(self.style_layers, style_outputs)}
return {'content':content_dict, 'style':style_dict}
该函数将返回一个包含两个元素的字典— content_dict包含内容层及其输出,而style_dict包含样式层及其输出。您可以使用此功能:
extractor = StyleContentModel(style_layers, content_layers)
然后:
style_targets = extractor(style_image)['style']
content_targets = extractor(content_image)['content']
这样,当应用于不同的图像时,我们可以得到不同层的输出。请记住,当应用于我们的梵高画作时,我们需要样式层的输出,但当应用于风景(或您的图像)图像时,我们需要内容层的输出。让我们将内容图像(风景或您的图像)保存在一个变量中,并定义一个函数(它将在后面 9 中有用),该函数将在 0 和 1 之间裁剪数组的值:
image = tf.Variable(content_image)
def clip_0_1(image):
return tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0)
那么我们可以将两个变量 α 、 β 定义如下:
style_weight=1e-2
content_weight=1e4
现在我们有了定义损失函数所需的一切:
def style_content_loss(outputs):
style_outputs = outputs['style']
content_outputs = outputs['content']
style_loss = tf.add_n([tf.reduce_mean((style_outputs[name]-style_targets[name])**2)
for name in style_outputs.keys()])
style_loss *= style_weight / num_style_layers
content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2)
for name in content_outputs.keys()])
content_loss *= content_weight / num_content_layers
loss = style_loss + content_loss
return loss
这段代码是不言自明的,因为我们已经讨论过它的各个部分。这个函数期望我们使用StyleContentModel类获得的字典作为输入。
现在让我们创建一个更新权重的函数:
@tf.function()
def train_step(image):
with tf.GradientTape() as tape:
outputs = extractor(image)
loss = style_content_loss(outputs)
grad = tape.gradient(loss, image)
opt.apply_gradients([(grad, image)])
image.assign(clip_0_1(image))
我们使用tf.GradientTape来更新图像。注意,当你用@tf.function注释一个函数时,你仍然可以像调用其他函数一样调用它。但是会被编译成图,这意味着你获得了更快执行的好处,在 GPU 或者 TPU 上运行,或者导出到 SavedModel(参见 https://www.tensorflow.org/alpha/guide/autograph )。请记住,变量extractor是通过以下代码获得的:
extractor = StyleContentModel(style_layers, content_layers)
并且是具有不同层的输出的字典。
现在,这段代码在开始时理解起来相当高级和复杂,所以不要着急,同时打开 Jupyter 笔记本阅读页面,以便能够理解代码和解释。如果一开始你不明白所有的事情,不要气馁。该行:
grad = tape.gradient(loss, image)
将计算损失函数相对于我们已经定义的变量image的梯度。每个更新步骤都可以通过一行简单的代码来完成:
train_step(image)
现在我们可以轻松地进行最后一个循环了:
epochs = 20
steps_per_epoch = 100
step = 0
for n in range(epochs):
for m in range(steps_per_epoch):
step += 1
train_step(image)
print(".", end=")
display.clear_output(wait=True)
imshow(image.read_value())
plt.title("Train step: {}".format(step))
plt.show()
当它运行时,你会看到图像每一个时代都在变化,你可以见证它是如何变化的。
有剪影的 NST
你可以用 NST 做一个有趣的应用,它与剪影 12 有关。一个剪影是一个用单一颜色的固体形状表示的图像。在图 5-3 中,可以看到一个例子;如果你是*星球大战的粉丝,*你知道是谁(提示:达斯维达 13 )。
图 5-3
《星球大战》角色达斯·维德的剪影
你应该在互联网上搜索类似马赛克或彩色玻璃的图像,如图 5-4 所示。
图 5-4
马赛克般的图像
目标是获得如图 5-5 所示的图像。
图 5-5
NST 在应用蒙版后在剪影上完成(稍后会详细介绍)
掩饰
屏蔽有几种含义,取决于您使用它的领域。这里我指的蒙版是根据剪影把图像的部分变成绝对白色的过程。这个想法在图 5-6 中进行了图示。你可以这样想:你在你的图像上放一个剪影(它们应该有相同的分辨率),只保留剪影是黑色的部分。
图 5-6
应用于图 5-4 中镶嵌图像的遮蔽
这没问题,但是有点不满意,因为例如你在结果中没有边。马赛克形状简单地从中间切开。视觉上这不是很令人满意。但是我们可以使用 NST 来使最终图像更好。该过程如下:
-
您使用类似马赛克的图像作为样式图像。
-
您使用您的轮廓图像作为内容图像。
-
最后,使用剪影图像将遮罩应用到最终结果中。
你可以在图 5-5 中看到结果(使用相同的代码)。你可以看到你得到了很好的边缘,马赛克瓷砖没有被切成两半。
你可以在本书第五章的 GitHub 知识库中找到完整的代码。但是作为参考,让我们假设您将图像保存为 numpy 数组。让我们假设剪影保存在一个名为mask的数组中,而你的图像保存在一个名为result的数组中。假设(您应该检查一下)掩码数组将只包含 0 或 255 个值(黑和白)。然后简单地用这个做屏蔽:
result[mask] = 255
这只是使白色的结果图像中有白色的轮廓,其余的保持不变。
Footnotes 1https://en.wikipedia.org/wiki/Loss_function
2
这个例子在 Michelucci,Umberto,2018 中有详细讨论。应用深度学习:基于案例的理解深度神经网络的方法。1.奥弗拉格。纽约:新闻。国际标准书号 978-1-4842-3789-2。可从: https://doi.org/10.1007/978-1-4842-3790-8
3
记住维度的顺序取决于你如何构建你的网络,你可能需要改变它。此处的尺寸仅用于说明目的。
4
https://en.wikipedia.org/wiki/Taylor_series
5
https://en.m.wikipedia.org/wiki/Skewness .在E**δY= 0 的情况下。
[6
https://en.m.wikipedia.org/wiki/Kurtosis 。在E[δY]= 0 的情况下。
7
在概率和统计中,概率质量函数(PMF)是给出离散随机变量恰好等于某个值的概率的函数【Stewart,William J. (2011)。 概率、马尔可夫链、队列、模拟:性能建模的数学基础 *。*普林斯顿大学出版社。第 105 页。ISBN978-1-4008-3281-1。]
8
https://en.wikipedia.org/wiki/Claude_Shannon
9
https://en.wikipedia.org/wiki/Neural_Style_Transfer
10
莱昂·加蒂斯;亚历山大·埃克;马蒂亚斯·贝斯吉(2015 年 8 月 26 日)。《艺术风格的神经算法》。https://arxiv.org/abs/1508.06576
11
“用于大规模视觉识别的非常深的 CNN”。Robots.ox.ac.uk。2014.检索到 2019 年 2 月 13 日, http://www.robots.ox.ac.uk/~vgg/research/very_deep/
12
本章的这一部分受到了中帖 https://becominghuman.ai/creating-intricate-art-with-neural-style-transfer-e5fee5f89481 的启发。
13
https://en.wikipedia.org/wiki/Darth_Vader
14
请注意,本章中使用的所有图像都是无版权和免费使用的图像。如果你在你的论文或作品中使用图像,确保你可以自由使用它们,否则你需要支付版税。
****