赞
踩
卷积神经网络(CNN),是一种专门用来处理类似网格结构数据的神经网络,例如图像数据。卷积神经网络一词表明该网络使用了卷积这种数学运算。卷积网络是指那些至少在网络的一层中使用卷积运算来替代一般矩阵乘法运算的神经网络。卷积神经网络需要的参数少于全连接架构的网络,而且卷积也很容易用 GPU 并行计算。因此卷积神经网络除了能够高效地采样从而获得精确的模型,还能够高效地计算。
多层感知机十分适合处理表格数据,但对于高维感知数据,这种缺少结构的网络可能会变得不实用。例如在处理图像数据时,每张照片可能具有百万级像素,意味着百万维度的输入。即使将隐藏层维度降到 1000,这个全连接层也将有 1 0 6 × 1 0 3 = 1 0 9 10^6 \times 10^3=10^9 106×103=109 个参数,想要训练这个模型非常困难。即使将输入降到十万像素,1000 隐藏单元的隐藏层可能也学不到有用的表示,还是需要增大隐藏单元数目,最后也需要大量的参数。
图像中本就拥有丰富的结构,而这些结构可以被人类和机器学习模型使用。卷积神经网络是机器学习利用自然图像中的一些已知结构的创造性方法。
全连接层和卷积层的根本区别在于,Dense
层从输入特征空间中学到的是全局模式(涉及所有像素),而卷积层学到的是局部模式,这个特性使得卷积神经网络具有如下性质:
我们还可以从稀疏交互以及参数共享的方面来理解卷积神经网络。
我们知道传统的神经网络使用矩阵乘法来建立输入与输出的连接关系,其中参数矩阵中每一个单独的参数都描述了一个输入单元与一个输出单元间的交互。然而,卷积网络具有稀疏交互的特征。这是使核的大小远小于输入的大小来达到的。对于一张数万计像素点的图像,我们只需用一个占用几十个像素点的核就能检测到一些有意义的特征,例如上图中检测到的边缘。这意味着我们需要存储的参数更少。而且随着网络不断加深,网络深层的单元可能与绝大部分输入是间接交互的(下面提到的感受野)。
在卷积神经网络中,核的每一个元素都作用在输入的每一个位置上(忽略步幅、填充等问题),我们可以把此理解为参数共享。在传统的神经网络中,当计算一层的输出时,权重矩阵的每一个元素只使用一次,当它乘以一个输入单元后就再也不会用到了。卷积神经网络的参数共享也从另一个角度说明了它对模型存储需求的显著降低。
在数学中,两个函数之间的卷积被定义为:
(
f
∗
g
)
(
x
)
=
∫
f
(
z
)
g
(
x
−
z
)
d
z
(f*g)(\boldsymbol{x})=\int f(\boldsymbol{z})g(\boldsymbol{x}-\boldsymbol{z})d\boldsymbol{z}
(f∗g)(x)=∫f(z)g(x−z)dz
也就是说,卷积是当把一个函数翻转并移位
x
\boldsymbol{x}
x 时,测量
f
f
f 和
g
g
g 之间的重叠,当为离散对象时,积分就变为求和。
在卷积网络的术语中,卷积的第一个参数(上式中的 f f f)通常叫做输入,第二个参数( g g g)叫做核函数,输出有时被称为特征映射(feature map)。卷积是可交换的,是因为我们将核相对输入进行了翻转(flip)。尽管可交换性在证明时很有用,但在神经网络的应用中并不是一个重要的性质。许多神经网络实现的则是互相关函数,和卷积运算几乎一样但是没有对核进行翻转。许多机器学习的库实现的是互相关函数但是称之为卷积。因此严格来说,卷积层是个错误的叫法。在卷积层中,输入张量和核张量通过互相关运算产生输出张量。
但因为卷积核权重是从数据中学习到的,因此无论这些层执行严格的卷积运算还是互相关运算,卷积层的输出都不会受到影响。
一个二维互相关运算的例子:
核函数从输入张量的左上角开始,从左到右、从上到下滑动。包含在核函数窗口内的元素与核函数对应位置的元素进行按元素相乘,得到的张量再求和得到一个单一的标量值。卷积核只与图像中每个大小完全适合的位置进行互相关运算,因此输出大小等于输入大小减去卷积核大小再加上 1。
import torch
from torch import nn
def corr2d(X, K):
"""计算二维互相关运算"""
h, w = K.shape
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) # 输出大小等于输入大小减去卷积核大小再加上 1
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i+h, j:j+w] * K).sum()
return Y
我们可以验证上图中的运算:
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)
"""
Out: tensor([[19., 25.],
[37., 43.]])
"""
卷积层对输入和卷积核权重进行互相关运算,并在添加标量偏置之后产生输出,卷积核权重被随机初始化:
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
如之前所说,卷积层的输出有时被称为特征映射,因为它可以被视为一个输入映射到下一层的空间维度的转换器。在卷积神经网络中,对于某一层的任意元素 x x x,其感受野(receptive field)是指在前向传播期间可能影响 x x x 计算的所有元素(来自所有先前层)。
还是以上面计算二维互相关的图为例:
因此,当一个特征图中的任意元素需要检测更广区域的输入特征时,我们可以构建一个更深的网络。
假设我们有一个 5 × 5 的特征图,如果我们卷积核的尺寸是 3 × 3,那么输出特征图的尺寸是 3 × 3,沿着每个维度都正好缩小了两个方块。这是因为我们卷积窗口的中心只能位于 5 × 5 特征图中间的 9 个 块上(如下图),最外层的方块(渐变色)无法作为 3 × 3 卷积核的中心:
如果想要输出特征图维度与输入特征图相同,那么我们就必须把图像的每个像素都要当作卷积核中心。既然以最外围的方块为中心的卷积核有覆盖到不属于图像的区域,一个直观的想法便是将该部分区域也当作图像区域,这就是填充(padding)。填充是在输入特征图的每一边添加适当数目的行和列(通常填充元素是 0),使得每个输入方块都能作为卷积窗口的中心(卷积窗口维数为奇数)。对于上图中 3 × 3 的窗口大小,我们只需要在输入特征图左右各添加一列,上下各添加一行。对于 5 × 5 的窗口,则需要各添加两行和两列。填充后特征图输出也有可能会大于原来的尺寸:
假设输入形状为
i
h
×
i
w
i_h \times i_w
ih×iw,卷积核形状为
k
h
×
k
w
k_h \times k_w
kh×kw,我们前面计算过,在没有填充的情况下,输出形状为
(
i
h
−
k
h
+
1
)
×
(
i
w
−
k
w
+
1
)
(i_h-k_h +1) \times (i_w-k_w+1)
(ih−kh+1)×(iw−kw+1)。
如果我们添加
p
h
p_h
ph 行填充(一半顶部,一半底部)和
p
w
p_w
pw 列填充(左右各一半),那么输出形状将变为
(
i
h
−
k
h
+
p
h
+
1
)
×
(
i
w
−
k
w
+
p
w
+
1
)
(i_h-k_h +p_h +1) \times (i_w-k_w + p_w +1)
(ih−kh+ph+1)×(iw−kw+pw+1)
即输出的高度和宽度分别增加了 p h p_h ph 和 p w p_w pw。
卷积神经网络中卷积核宽度和高度通常为奇数,例如 1、3、5、7。选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列。因此通常情况下 p h = p w = p p_h=p_w=p ph=pw=p。偶数大小的卷积核情况可能较为复杂,输出形状也不容易计算。
对于 keras 中的 Conv2D
层,可以通过 padding
参数来设置填充,这个参数有两个取值:valid
表示不使用填充,same
表示填充后输出的宽度和高度与输入相同。默认参数值为 valid
。
而在 Pytorch 中的 Conv2D
层,我们需手动为参数 padding
指明填充的行数和列数,例如对于高度为 5,宽度为 3 的卷积核,为了使输出和输入具有相同的高度和宽度,我们可以指定 padding
值为 (2, 1)
,即高度和宽度两边的填充分别为 2 和 1。
我们知道卷积窗口需从左到右、从上到下在输入张量上滑动。前面的例子中,我们都默认每次滑动一个元素。但是,有时为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素,快速减小特征图尺寸。我们将每次滑动元素的数量称为步幅(stride)。例如,在不考虑填充的情况下,我们将步幅设置为 2,这意味着特征图的宽度和高度都被做了 2 倍下采样。
理论上我们可以将垂直步幅
s
h
s_h
sh 和 水平步幅
s
w
s_w
sw 设置为不同的大小,但实践中,和填充一样,我们很少使用不一致的步幅,也就是说,通常我们有
s
h
=
s
w
=
s
s_h=s_w=s
sh=sw=s。
目前为止我们仅仅考虑了单输入单输出通道的情况。但实际中我们通常会处理类似 RGB 图像这样的 3 维张量,它除了高度和宽度,还有通道数这个维度。
因此当输入包含多个通道时,我们需要构造一个与输入数据具有相同输入通道数的卷积核,以便与输入数据进行互相关运算。假设输入的通道数为 c c c,那么卷积核的输入通道数也需要为 c c c。由于输入和卷积核都有 c c c 个通道,我们可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(比如有 3 个通道,那我们就将 3 个通道各自得到的结果相加)得到二维张量。下面我们实现一下多输入通道的互相关运算:
import torch
def corr2d_multi_in(X, K):
# X,K 的第0个维度便是通道维度,与 keras 有所区别
return sum(corr2d(x, k) for x, k in zip(X, K))
"""输入为 2 通道"""
X = torch.tensor([[[0, 1, 2], [3, 4, 5], [6, 7, 8]],
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]])
K = torch.tensor([[[0, 1], [2, 3]],
[[1, 2], [3, 4]]])
corr2d_multi_in(X, K)
"""
Out: tensor([[ 56., 72.],
[104., 120.]])
"""
在最流行的神经网络架构中,随着神经网络层数的加深,我们会增加输出通道的维数,通过减少空间分辨率以获得更大的通道深度。直观的说,我们可以将每个通道看作是对不同特征的响应。有时也把每个输出通道叫做过滤器(filter),代表其从输入数据的某一方面进行编码。
⽤ c i c_i ci 和 c o c_o co 分别表示输入和输出通道的数目,并让 k h k_h kh 和 k w k_w kw 为卷积核的高度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建⼀个形状为 c i × k h × k w c_i \times k_h \times k_w ci×kh×kw 的卷积核张量,这样卷积核的形状是 c o × c i × k h × k w c_o \times c_i \times k_h \times k_w co×ci×kh×kw。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。(不同输出通道的卷积核会学习不同的权重,即会学到不同的特征)
下面我们实现一个计算多通道输出的互相关函数。
def corr2d_multi_in_out(X, K):
# 迭代 K 的第0个维度(输出通道数),每次都对 X 执行互相关运算
# torch.stack: 把每次 corr2d_multi_in 计算得到的二维张量凭借成一个三维张量
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
"""借助上例中的K构建一个3个输出通道的卷积核"""
K = torch.stack((K, K+1, K+2), 0)
corr2d_multi_in_out(X, K)
"""
Out: tensor([[[ 56., 72.],
[104., 120.]],
[[ 76., 100.],
[148., 172.]],
[[ 96., 128.],
[192., 224.]]])
"""
可以看到第一个输出通道的结果与先前输入张量 X 和多输入单输出通道的结果一致。
如果理解了以上所有操作,那么我们可以最终得出计算卷积层输出维度的公式:
输入数据矩阵尺寸: W 1 × H 1 × C 1 W_1\times H_1 \times C_1 W1×H1×C1
输出特征图组尺寸: W 2 × H 2 × C 2 W_2\times H_2 \times C_2 W2×H2×C2
其中,
W
2
=
(
W
1
−
k
+
p
)
/
s
+
1
W_2=(W_1-k+p)/s+1
W2=(W1−k+p)/s+1
H 2 = ( H 1 − k + p ) / s + 1 H_2=(H_1-k+p)/s+1 H2=(H1−k+p)/s+1
C 2 = F C_2=F C2=F
k
k
k 是卷积核尺寸,
s
s
s 是卷积步长,
p
p
p 是填充数量,
F
F
F 是卷积核个数(输出通道数)。
1 × 1 卷积层看起来似乎没有多大意义,卷积的本质是有效提取相邻像素间的相关特征,显然 1 × 1 卷积层没有此作用,但它仍然十分流行,经常包含在复杂深层网络的设计中。
下图展示了使用 3 输入通道和 2 输出通道的 1 × 1 卷积核的互相关计算:
这里输入和输出具有相同的高度和宽度,输出中的每个元素都是输入图像中同一位置元素的线性组合。我们可以将 1 × 1 卷积层看作是在每个像素位置应用的全连接层,以
c
i
c_i
ci 个输入值转换为
c
o
c_o
co 个输出值。下面我们用全连接层实现 1 × 1 卷积。
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
# 将每个二维张量展平为一维
X = X.reshape((c_i, h*w))
K = K.reshape((c_o, c_i))
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))
这与之前实现的互相关函数 corr2d_multi_in_out
是等价的。
卷积网络中一个典型层包含三级:
通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野就越大。
而我们的机器学习任务通常会跟全局图像的问题有关(例如,“图像是否包含一只猫?”),所以我们最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。
不管采用什么样的池化函数,当输入作出少量平移时,池化能够帮助输入的表示近似不变。下图说明了这是如何实现的。局部平移不变性是一个很有用的性质,尤其是当我们关心某个特征是否出现而不关心它出现的具体位置时。
我们可以看到,当输入右移了一个像素之后,所有输入值都发生了改变,但池化的输出只有一半发生了变化。
简而言之,使用池化来进行下采样的原因,一是减少需要处理的特征图的元素个数,二是让连续卷积层的观察窗口越来越大(增大感受野),让最后卷积层的特征包含输入的整体信息。虽然卷积层中的步幅也可以实现下采样,但效果不如池化函数好,因为它可能错过一些重要的特征信息。
与卷积层类似,池化层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为池化窗口)遍历的每个位置计算一个输出。然而,不同于卷积层中的输入与卷积核之间的互相关计算,池化层不包含参数。相反,池运算是确定性的,我们通常计算池化窗口中所有元素的最大值或平均值。这些操作分别称为最大池化(maximum pooling)和平均池化(average pooling)。
最大池化往往比平均池化能给出更多的信息,原因在于特征中往往编码了某种模式或概念在特征图的不同位置是否存在。
池化层也有填充和步幅的概念,与卷积层别无二致。在处理多通道输入时,汇聚层在每个输入通道上单独运算,这意味着汇聚层的输出通道数与输入通道数相同。
import torch
from torch import nn
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i:i+p_h, j:j+p_w].max()
elif mode == 'avg':
Y[i, j] = X[i:i+p_h, j:j+p_w].mean()
return Y
我们可以验证上例中 2× 2 最大池化的结果是否正确:
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
"""
Out: tensor([[4., 5.],
[7., 8.]])
"""
我们可以先自行计算一下每层的输出张量形状,然后再用 model.summary()
验证。
from keras import layers
from keras import models
model = models.Sequential()
"""卷积层的第一个参数便是上面讨论的卷积核输出通道数"""
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1))) # 注意通道数在第 2 维
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2, 2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
model.summary()
关于不同优化算法的具体细节与不同,可以看这篇文章,《深度模型中的优化(二)》。
from keras.datasets import mnist
from keras.utils import to_categorical
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype('float32') / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype('float32') / 255
"""独热编码"""
train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)
model.compile(optimizer='rmsprop',
loss='categorical_crossentropy',
metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)
model.evaluate(test_images, test_labels)
import torch
from torch import nn
"""
Conv2d(in_channels, out_channels, kernel_size:, stride,
padding, dilation, groups, bias:,
padding_mode: str='zeros', device=None, dtype=None)
"""
net = nn.Sequential(
nn.Conv2d(1, 6, kernel_size=5, padding=2),
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Conv2d(6, 16, kernel_size=5),
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=2, stride=2),
nn.Flatten(),
nn.Linear(16*5*5, 120),
nn.Sigmoid(),
nn.Linear(120, 84),
nn.Sigmoid(),
nn.Linear(84, 10)
)
我们可以构建一个随机输入,查看模型每一层的输出形状:
X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
X = layer(X)
print(layer.__class__.__name__, 'output shape: \t', X.shape)
我们可以看到最后一个平均池化层输出形状为 [1, 16, 5, 5]
,这也是为什么第一个全连接层的输入特征数写为 16*5*5
的原因。
"""加载数据集"""
import torchvision
from torch.utils import data
from torchvision import transforms
def load_data_fashion_mnist(batch_size, resize=None):
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, shuffle=True),
data.DataLoader(mnist_test, batch_size, shuffle=False))
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size=batch_size)
LeNet 是最早发布的卷积神经网络之一。其采用了 sigmoid
和平均池化,虽然 relu
和 最大池化可能更高效,但它们在当时还没有出现!因此训练的精度不会太高。
import torch.optim as optim
import time
def train(net, device, epochs=100):
optimizer = optim.Adam(net.parameters(), lr=0.0001,)
loss = nn.CrossEntropyLoss()
for Epoch in range(epochs):
t0 = time.time()
running_loss = 0.0
total = 0
correct = 0
for i, data in enumerate(train_iter, 0):
inputs, labels = data
inputs, labels = inputs.to(device),labels.to(device)
optimizer.zero_grad()
outputs = net(inputs)
l = loss(outputs, labels)
l.backward()
optimizer.step()
running_loss += l.item()
if i % 200 == 199: # print every 200 mini-batches
print('[%d, %5d] loss: %.4f' %
(Epoch+1, i, running_loss / 500))
running_loss = 0.0
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('LeNet accuracy on the %d tran images is: %.3f %%' % (total,
100.0 * correct / total))
total = 0
correct = 0
print('Epoch %d cost %.3f sec' % (Epoch+1, time.time() - t0))
print('Training Finished')
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
net = net.to(device)
train(net, device)
卷积网络也许是生物学启发人工智能的最为成功的案例。虽然卷积网络也经过许多其他领域的指导,但是神经网络的一些关键设计原则来自于神经科学。
卷积网络的历史始于神经科学实验,远早于相关计算模型的发展。为了确定关于哺乳动物视觉系统如何工作,神经生理学家 David Hubel 和 Torsten Wiesel 合作多年,他们的成就最终获得了诺贝尔奖。他们的发现对当代深度学习模型有最大影响的是基于记录猫的单个神经元的活动。他们观察了猫的脑内神经元如何响应投影在猫前面屏幕上精确位置的图像。他们的伟大发现是,处于视觉系统较为前面的神经元对非常特定的光模式(例如精确定向的条纹)反应最强烈,但对其他模式几乎完全没有反应。
他们的工作有助于表征大脑功能的许多方面。从深度学习的角度来看,我们可以专注于简化的、草图形式的大脑功能视图。
在这个简化的视图中,我们关注被称为 V1 的大脑的一部分。也称为初级视觉皮层(primary visual cortex)。V1 是大脑对视觉输入开始执行显著高级处理的第一个区域。在该草图视图中,图像是由光到达眼睛并刺激视网膜形成的。视网膜中的神经元对图像执行一些简单的预处理,但是基本不改变它被表示的方式。然后图像通过视神经和称为外侧膝状核的脑部区域。这些解剖区域的主要作用是仅仅将信号从眼睛传递到位于头后部的 V1。
卷积网络层被设计为描述 V1 的三个性质:
[1] 《动手学深度学习》,Aston Zhang, Zachary C. Lipton, Mu Li, and Alexander J. Smola.
[2] 《Python 深度学习》,François Chollet.
[3] I. J. Goodfellow, Y. Bengio, and A. Courville, Deep Learning. Cambridge, MA, USA: MIT Press, 2016, http://www.deeplearningbook.org.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。