在过去的几年里,关于卷积神经网络(Convolution Neural Networks,CNN)的讨论越来越多,特别是因为它彻底改变了计算机视觉领域。在本文中,我们将基于神经网络的基本背景知识,探索CNN是什么,了解它们的工作原理,并在Python中从头开始构建一个真正的CNN(仅使用numpy)。
这篇文章仅假设你拥有神经网络的基本知识。我对神经网络的介绍涵盖了您需要了解的所有内容,因此您可能需要首先阅读它。
为什么使用CNN
CNN的经典案例是图像分类,例如查看宠物的图像并确定它是猫还是狗。那么问题来了,为什么不使用普通的神经网络?
好问题
原因1:图片很大
如今,有关计算机视觉问题的图像通常为224x224或更大。想象一下,建立一个神经网络来处理224x224彩色图像:包括图像中的3个彩色通道(RGB),也就是说需要224 x 224 x 3 = 150528个输入特征!在这种网络中,典型的隐藏层可能有1024个节点,因此仅第一层就必须训练150,528 x 1024 = 150+百万个权重。我们的网络非常庞大,几乎无法训练。
而且我们也不需要那么多的权重。我们知道像素在与其相邻的环境中最有用。图像中的对象由小的局部特征组成,例如眼睛的圆形虹膜或一张纸的方形角。第一个隐藏层中的每个节点查看每个像素是否很浪费?
原因2:位置会发生变化
如果你训练了一个网络来检测狗,你会希望它能够检测狗,而不管它出现在图像的什么地方。想象一下,训练一个网络,它能很好地处理一个特定的狗的图像,但是给它一个稍微改变了的图像。狗不会激活相同的神经元,所以网络的反应会完全不同!
我们很快就会看到CNN如何帮助我们解决这些问题。
数据集
在本文中,我们将解决计算机视觉的“Hello, World!”:MNIST手写数字分类问题。很简单:给定图像,将其分类为数字。
MNIST数据集
MNIST数据集中的每个图像均为28x28像素,并包含居中的灰度数字。
说实话,一般的神经网络实际上可以很好地解决这个问题。您可以将每张图像视为28 x 28 = 784维向量,将其输入到784维输入层,堆叠一些隐藏层,最后得到10个节点的输出层,每个数字1个节点。
但是它只会在MNIST数据集包含居中的小图像时起作用,本例中我们不会遇到上述尺寸或偏移问题。请记住,大多数现实世界中的图像分类问题并非易事。
卷积
什么是卷积神经网络?
它是使用卷积层(又称为Conv层)的神经网络,其基于卷积的数学运算。Conv层由一组filter(卷积核)组成,您可以将其视为二维的数字矩阵。这是一个3x3卷积核示例:
我们可以输入图像和卷积核,将卷积核与输入图像卷积来生成输出图像。这包括
- 在某些位置将卷积核覆盖在图像顶部。
- 在卷积核中的值和图像中相应的值之间执行逐元素乘法(element-wise multiplication)。
- 加总所有逐元素乘法的结果。和是输出图像中目标像素的输出值。
- 在所有位置重复该过程。
旁注:从技术上讲,我们(以及许多CNN)实际上在这里使用互相关而不是卷积,但是它们几乎是同一件事。我不会在这篇文章中介绍差异,因为它并不重要,但是如果您感到好奇,可以随时查找相关资料。
这个四步描述有点抽象,所以让我们做一个例子。考虑一下这个微小的4x4灰度图像和3x3卷积核:
4x4图像(左)和3x3卷积核(右)
图像中的数字表示像素强度,其中0为黑色,255为白色。我们将对输入图像和卷积核进行卷积以生成2x2的输出图像:
首先,让我们将卷积核覆盖在图像的左上角:
步骤1:将卷积核(右)覆盖在图像(左)的上方
接下来,我们在重叠的图像值和卷积核值之间执行逐元素乘法。结果如下,从左到右,从上到下:
图像值 | 卷积核值 | 结果 |
---|---|---|
0 | -1 | 0 |
50 | 0 | 0 |
0 | 1 | 0 |
0 | -2 | 0 |
80 | 0 | 0 |
31 | 2 | 62 |
33 | -1 | -33 |
90 | 0 | 0 |
0 | 1 | 0 |
步骤2:执行逐元素乘法。
接下来,加总所有的结果,这很容易
$$ 62 - 33 = \boxed{29} $$
最后,我们将结果放置在输出图像的目标像素中。由于我们的卷积核覆盖在输入图像的左上角,因此我们的目标像素是输出图像的左上像素:
重复以上步骤,得到其他位置像素
它有什么用?
让我们缩小一下,在更高的层次上看。将一个图像与一个卷积核进行卷积有什么用?我们可以从我们一直使用的3x3卷积核开始,它通常被称为垂直索伯滤波器
垂直索伯滤波器
这是一个垂直索伯滤波器的示例:
图像与垂直索伯滤波器卷积
类似地,还有一个水平索伯滤波器卷积:
水平索伯滤波器卷积
图像与水平索伯滤波器卷积
看看发生了什么事?索伯滤波器是边缘检测算子(edge-detectors)。垂直索伯滤波器检测垂直边缘,而水平索伯滤波器检测水平边缘。现在可以轻松地解释输出图像:输出图像中的亮像素(值较高的像素)表明原始图像周围有很强的边缘。
您能看到为什么边缘检测图像比原始图像更有用的原因吗?回想一下我们的MNIST手写数字分类问题。经过MNIST训练的CNN可能会通过使用边缘检测算子,并检查图像中心附近的两个突出的垂直边缘来寻找数字1。通常,卷积可以帮助我们寻找特定的局部图像特征(例如边缘),以便以后在网络中使用。
填充(Padding)
还记得将4x4输入图像与3x3卷积核进行卷积以生成2x2的输出图像吗?通常,我们希望输出图像的大小与输入图像的大小相同。为此,我们在图像周围添加零,以便可以在更多位置覆盖卷积核。3x3卷积核需要填充1个像素:
4x4输入与3x3卷积核卷积产生4x4输出
这被称为“相同”填充,因为输入和输出具有相同的尺寸。不使用任何填充(这是我们一直在做的),有时也称为“有效”填充。
卷积层
既然我们知道了图像卷积的工作原理以及为什么它有用,那么让我们看看它在CNN中的实际用法。如前所述,CNN包括使用一组卷积核将输入图像转换为输出图像的卷积层。卷积层的主要参数是一系列卷积核。
对于我们的MNIST CNN,我们将使用带有8个卷积核的小型卷机层作为网络的初始层。这意味着它将把28x28的平面输入图像变成26x26x8的立体输出图像:
提醒:输出是26x26x8而不是28x28x8,因为我们使用的是有效的padding,它将输入的宽度和高度减少2。
卷积层中的8个卷积核中的每一个都产生26x26的输出,因此堆叠在一起就构成了26x26x8的立体。3*3(卷积核的大小)*8(卷积核数量)=72,也就是说我们只需要72个权重!
实现卷积
我们将实现一个conv层的前馈部分,该部分负责将卷积核与输入图像进行卷积以产生输出。为简单起见,我们假定卷积核始终为3x3(事实并非如此-5x5和7x7卷积核也很常见)。
# Header: conv.py
import numpy as np
class Conv3x3:
# A Convolution layer using 3x3 filters.
def __init__(self, num_filters):
self.num_filters = num_filters
# filters is a 3d array with dimensions (num_filters, 3, 3)
# We divide by 9 to reduce the variance of our initial values
self.filters = np.random.randn(num_filters, 3, 3) / 9
Conv3x3
类只接受一个参数:卷积核的数量。在init函数中,我们存储卷积核的数量,并使用NumPy的randn()
方法初始化一个随机卷积核数组。
注意:在初始化过程中,除以9会比您想象的要重要。如果初始值太大或太小,训练网络将无效。要了解更多信息,请阅读Xavier Initialization。
接下来,实际的卷积:
# Header: conv.py
class Conv3x3:
# ...
def iterate_regions(self, image):
'''
Generates all possible 3x3 image regions using valid padding.
- image is a 2d numpy array
'''
h, w = image.shape
for i in range(h - 2):
for j in range(w - 2):
im_region = image[i:(i + 3), j:(j + 3)]
yield im_region, i, j
def forward(self, input):
'''
Performs a forward pass of the conv layer using the given input.
Returns a 3d numpy array with dimensions (h, w, num_filters).
- input is a 2d numpy array
'''
h, w = input.shape
output = np.zeros((h - 2, w - 2, self.num_filters))
for im_region, i, j in self.iterate_regions(input):
output[i, j] = np.sum(im_region * self.filters, axis=(1, 2)) # highlight-line
return output
python›iterate_regions()
是一种辅助生成器方法,可以为我们生成所有有效的3x3图像区域。 对于该类的后半部分非常有用。
上面突出显示了实际执行卷积的代码行。让我们分解一下:
im_region
, 一个包含相关图像区域的3x3数组.self.filters
, 一个三维数组.- 代码
im_region * self.filters
, 利用了numpy的 broadcasting (广播)功能将两个数组逐元素相乘 。其结果是一个三维数组,与self.filters
维度相同. - 使用np.sum()
axis=(1, 2)
对上一步得到的结果相加,生成了一个一维数组num_filters
,其中每个元素包含对应卷积核的卷积结果 - 将结果分配给
output[i, j]
, 其中包含输出图像中像素的卷积结果
对输出中的每个像素执行以上过程,直到获得最终的输出!代码如下:
# Header: cnn.py
import mnist
from conv import Conv3x3
# The mnist package handles the MNIST dataset for us!
# Learn more at https://github.com/datapythonista/mnist
train_images = mnist.train_images()
train_labels = mnist.train_labels()
conv = Conv3x3(8)
output = conv.forward(train_images[0])
print(output.shape) # (26, 26, 8)
到目前为止看起来不错。
注意:在我们的Conv3x3
中,为简单起见,我们假设输入为2维numpy数组,因为这就是我们的MNIST图像的存储方式。这对我们有用,因为我们将其用作网络的第一层,但是大多数CNN都具有更多的Conv层。如果我们要建立一个更大的网络,需要多次使用Conv3x3
,则必须使输入为三维numpy数组。
池化层(Pooling)
图像中的相邻像素往往具有相似的值,因此经过卷积层输出中的相邻像素也有相似的值。也就是卷机层输出中包含了大量多余的信息。例如,如果我们使用边缘检测滤波器,并在某个位置找到了较强的边缘,则很有可能在从原始像素偏移1个像素的位置上也找到了相对较强的边缘。但是,这些都是相同的边缘!我们没有发现任何新东西。
池化层解决了这个问题。他们所做的只是减小输入的大小,通过与池化层的值一起输入。该池通常是通过简单的操作,比如max
,min
或average
来实现。这是最大池化层的示例,池化大小为2:
在4x4图像上的最大池化(池大小2)产生2x2输出
为了执行最大池化(_max_ pooling
),我们以2x2块(因为池大小= 2)遍历输入图像,并将最大值放入对应像素的输出图像中而已!
池化将输入的宽度和高度变为除以池大小的宽度和高度。对于我们的MNIST CNN,我们将在初始转换层之后立即放置一个池大小为2的最大池化层。池化层会将26x26x8输入转换为13x13x8输出:
Pooling divides the input's width and height by the pool size. For our MNIST CNN, we'll place a Max Pooling layer with a pool size of 2 right after our initial conv layer. The pooling layer will transform a 26x26x8 input into a 13x13x8 output:
池化的代码实现
我们将用与上一节中的conv类相同的方法来实现一个 MaxPool2
类::
# Header: maxpool.py
import numpy as np
class MaxPool2:
# A Max Pooling layer using a pool size of 2.
def iterate_regions(self, image):
'''
Generates non-overlapping 2x2 image regions to pool over.
- image is a 2d numpy array
'''
h, w, _ = image.shape
new_h = h // 2
new_w = w // 2
for i in range(new_h):
for j in range(new_w):
im_region = image[(i * 2):(i * 2 + 2), (j * 2):(j * 2 + 2)]
yield im_region, i, j
def forward(self, input):
'''
Performs a forward pass of the maxpool layer using the given input.
Returns a 3d numpy array with dimensions (h / 2, w / 2, num_filters).
- input is a 3d numpy array with dimensions (h, w, num_filters)
'''
h, w, num_filters = input.shape
output = np.zeros((h // 2, w // 2, num_filters))
for im_region, i, j in self.iterate_regions(input):
output[i, j] = np.amax(im_region, axis=(0, 1)) # highlight-line
return output
该类的工作方式与Conv3x3
类似。要从给定图像区域中找到最大值,我们使用numpy的np.amax()方法。我们设定axis=(0, 1)
因为我们只想最大化前两个维度,即宽高,并不包括第三个维度:num_filters
(卷积核数量)
让我们测试一下!
# Header: cnn.py
import mnist
from conv import Conv3x3
from maxpool import MaxPool2
# The mnist package handles the MNIST dataset for us!
# Learn more at https://github.com/datapythonista/mnist
train_images = mnist.train_images()
train_labels = mnist.train_labels()
conv = Conv3x3(8)
pool = MaxPool2()
output = conv.forward(train_images[0])
output = pool.forward(output)
print(output.shape) # (13, 13, 8)
Softmax
要完成我们的CNN,我们需要赋予它实际进行预测的能力。我们将使用标准的最终层解决多类分类问题:Softmax层,这是一个全连接(密集)层,使用Softmax函数作为激活函数。
提醒:全连接层将每个节点连接到上一层的每个输出。我们在神经网络入门中就使用了全连接层。
如果您以前从未听说过Softmax,请先阅读我对Softmax的快速介绍,然后再继续。
使用
我们将使用带有10个节点的softmax层作为CNN的最后一层,每个节点代表一个数字。层中的每个节点将连接到每个输入。应用softmax转换后,由节点表示的概率最高的数字将是CNN的输出!
交叉熵损失(cross-entropy loss)
您可能想问,为什么要将输出转换为概率?最高的输出值会不会总是有最高的概率?如果您这样做,那绝对是正确的。我们实际上不需要使用softmax来预测数字 -我们可以选择网络中输出最高的数字!
softmax的真正作用是帮助我们量化对预测的置信度,这在训练和评估CNN时很有用。更具体地说,使用softmax让我们能用交叉熵损失,它考虑了我们对每个预测的确定程度。这是我们计算交叉熵损失的方法:
$$ L = -\ln(p_c) $$
其中c是正确的组 (在本例中,就是正确的数字), $p_c$ 是对c的预测概率, and $\ln$ is the 自然对数. 一般来说, 更低的损失会更好. 例如在完美的情况下,我们有:
$$ p_c = 1, L = -\ln(1) = 0 $$
在更真实的情况下,我们有
$$ p_c = 0.8, L = -\ln(0.8) = 0.223 $$
在后续的文章中我们仍会看到交叉熵损失。
Softmax代码实现
# Header: softmax.py
import numpy as np
class Softmax:
# A standard fully-connected layer with softmax activation.
def __init__(self, input_len, nodes):
# We divide by input_len to reduce the variance of our initial values
self.weights = np.random.randn(input_len, nodes) / input_len
self.biases = np.zeros(nodes)
def forward(self, input):
'''
Performs a forward pass of the softmax layer using the given input.
Returns a 1d numpy array containing the respective probability values.
- input can be any array with any dimensions.
'''
input = input.flatten()
input_len, nodes = self.weights.shape
totals = np.dot(input, self.weights) + self.biases
exp = np.exp(totals)
return exp / np.sum(exp, axis=0)
这里没有什么太复杂的。一些要点:
- 我们使用flatten() 使其更易使用, 因为我们不再需要它的形状.
- np.dot() 将
input
和self.weights
逐元素相乘,并将结果加总。. - np.exp() 计算用于Softmax的指数。
接下来我们把代码放到一起
# Header: cnn.py
import mnist
import numpy as np
from conv import Conv3x3
from maxpool import MaxPool2
from softmax import Softmax
# We only use the first 1k testing examples (out of 10k total)
# in the interest of time. Feel free to change this if you want.
test_images = mnist.test_images()[:1000]
test_labels = mnist.test_labels()[:1000]
conv = Conv3x3(8) # 28x28x1 -> 26x26x8
pool = MaxPool2() # 26x26x8 -> 13x13x8
softmax = Softmax(13 * 13 * 8, 10) # 13x13x8 -> 10
def forward(image, label):
'''
Completes a forward pass of the CNN and calculates the accuracy and
cross-entropy loss.
- image is a 2d numpy array
- label is a digit
'''
# We transform the image from [0, 255] to [-0.5, 0.5] to make it easier
# to work with. This is standard practice.
out = conv.forward((image / 255) - 0.5)
out = pool.forward(out)
out = softmax.forward(out)
# Calculate cross-entropy loss and accuracy. np.log() is the natural log.
loss = -np.log(out[label])
acc = 1 if np.argmax(out) == label else 0
return out, loss, acc
print('MNIST CNN initialized!')
loss = 0
num_correct = 0
for i, (im, label) in enumerate(zip(test_images, test_labels)):
# Do a forward pass.
_, l, acc = forward(im, label)
loss += l
num_correct += acc
# Print stats every 100 steps.
if i % 100 == 99:
print(
'[Step %d] Past 100 steps: Average Loss %.3f | Accuracy: %d%%' %
(i + 1, loss / 100, num_correct)
)
loss = 0
num_correct = 0
运行之后得到以下(类似)的输出
MNIST CNN initialized!
[Step 100] Past 100 steps: Average Loss 2.302 | Accuracy: 11%
[Step 200] Past 100 steps: Average Loss 2.302 | Accuracy: 8%
[Step 300] Past 100 steps: Average Loss 2.302 | Accuracy: 3%
[Step 400] Past 100 steps: Average Loss 2.302 | Accuracy: 12%
这是有道理的:通过初始化随机权重,您可以期望CNN与随机猜测一样好。随机猜测将产生10%的准确性(因为有10个类别),并且交叉熵损失为${-\ln(0.1)} = 2.302$,这就是我们得到的!
想亲自运行这段代码? 在浏览器中运行. 同样也可以在 Github上找到这段代码.
总结
到此为止,CNN的介绍到此结束!在这篇文章中,我们
- 阐述了CNN对于某些问题(例如图像分类)可能更有用的原因。
- 引入了MNIST手写数字数据集。
- 了解了Conv图层,该图层将卷积核与图像进行卷积以产生更多有用的输出。
- 讨论了Pooling层,它可以帮助去除最有用特征之外的所有内容。
- 实现了Softmax层,因此我们可以使用交叉熵损失。
我们还没有介绍更多内容,例如如何实际训练CNN。CNN系列的第2部分深入研究了CNN的训练,包括推导梯度和实现反向传播。另外,您还可以学习使用Keras(一个Python深度学习库)实现自己的CNN。
如果您急切希望看到训练有素的CNN:示例在MNIST上训练的Keras CNN可以达到99.25%的准确性。
版权属于:作者名称
本文链接:https://www.sitstars.com/archives/46/
转载时须注明出处及本声明