0%

卷积神经网络

卷积神经网络CNN(Convolutional Neural Network)

深度学习之卷积神经网络CNN

一. CNN的引入

在人工的全连接神经网络中,每相邻两层之间的每个神经元之间都是有边相连的。当输入层的特征维度变得很高时,这是全连接神经网络需要训练的参数就会增大很多,计算速度就会变得很慢。下图可以容易的看出全连接神经网络处理图像中需要的训练参数过多的问题。

1562759523740

而在卷积神经网络中,卷积层的神经元只与前一层的部分神经元节点相连,即它的神经元间的连接是非全连接的,且同一层中某些神经元之间的连接的权重w和偏移b是共享的(即相同的),这样大量地减少了需要训练参数的数量

卷积神经网络CNN的结构一般包含这几个层:

  • 输入层:用于数据的输入
  • 卷积层:使用卷积核进行特征提取和特征映射
  • 激励层:由于卷积也是一种线性运算,因此需要增加非线性映射
  • 池化层:进行下采样,对特征图稀疏处理,减少数据运算量
  • 全连接层:通常在CNN的尾部进行重新拟合,减少特征信息的损失
  • 输出层:用于输出结果

中间还可以使用一些其他的功能层:

  • 归一化层:在CNN中对特征进行归一化
  • 切分层:对某些(图片)数据进行分区域的单独学习
  • 融合层:对独立进行特征学习的分支进行融合

二. CNN的层次结构

输入层:

​ 在CNN的输入层中,(图片)数据输入的格式与全连接神经网络的输入格式(一维向量)不太一样。CNN的输入层的输入格式保留了图片本身的结构

对于黑白的28*28的图片,CNN的输入层是一个28 * 28的二维神经元

1562761433714

​ 而对于RGB格式的28×28图片,CNN的输入则是一个 3×28×28的三维神经元(RGB中的每一个颜色通道都有一个 28×2828×28 的矩阵),如下图所示:1562761477248

卷积层:

重要概念:

  • local receptive fields(感受视野)
  • shared weights(共享权值)

假设输入的是一个 28×282的的二维神经元,我们定义5×55×5 的 一个 local receptive fields(感受视野),即 隐藏层的神经元与输入层的5×5个神经元相连,这个5*5的区域就称之为Local Receptive Fields,如下图所示:

1562761574164

可类似看做:隐藏层中的神经元具有一个固定大小的感受视野去感受上一层的部分特征。在全连接神经网络中,隐藏层的神经元的感受视野足够大乃至可以看到上一层的所有特征

而在卷积神经网络中,隐藏层中的神经元的感受视野比较小,只能看到上一次的部分特征,上一层的其他特征可以通过平移感受视野来得到同一层的其他神经元,由同一层其他神经元来看:

1562761748425

设移动的步长为1:从左到右扫描,每次移动 1 格,扫描完之后,再向下移动一格,再次从左到右扫描。

具体过程如动图所示:设移动的步长为1:从左到右扫描,每次移动 1 格,扫描完之后,再向下移动一格,再次从左到右扫描。

CNN

可看出 卷积层的神经元是只与前一层的部分神经元节点相连,每一条相连的线对应一个权重w

一个感受视野带有一个卷积核,我们将感受视野中的权重w矩阵称为卷积核;将感受视野对输入的扫描间隔称为步长。当步长比较大时(stride>1),为了扫描到边缘的一些特征,感受视野可能会“出界”,这时需要对边界扩充(pad),边界扩充可以设为 0 或 其他值。步长 和 边界扩充值的大小由用户来定义。

卷积核的大小由用户来定义,即定义的感受视野的大小;卷积核的权重矩阵的值,便是卷积神经网络的参数,为了有一个偏移项 ,卷积核可附带一个偏移项 b ,它们的初值可以随机来生成,可通过训练进行变化。

因此 感受视野 扫描时可以计算出下一层神经元的值为:b+i=04j=04wijxijb+\sum_{i=0}^{4} \sum_{j=0}^{4} w_{i j} x_{i j}

对下一层的所有神经元来说,它们从不同的位置去探测了上一层神经元的特征。

我们将通过 一个带有卷积核感受视野 扫描生成的下一层神经元矩阵 称为 一个feature map (特征映射图),如下图的右边便是一个 feature map:

1562762187716

因此在同一个feature map上的神经元使用的卷积核是相同的,因此这些神经元shared weights,共享卷积核中的权值和附带的偏移。一个feature map对应一个卷积核,若我们使用三个不同的卷积核,可以输出三个feature map:(感受视野: 5 * 5,步长stride:1)

1562762359660

因此在CNN的卷积层,我们需要训练的参数大大地减少到了(5 *5 + 1) * 3 = 78个

假设输入的是28 *28的RGB图片,即输入的是一个3 * 28 * 28 的二维神经元,这是卷积大小不止用长和宽来表示,还有深度,感受视野也对应有了深度,如下图所示:

1562762829161

由图可知:感受视野: 3 * 2 * 2,卷积核: 3 *2 * 2,深度为3;下一层的神经元的值为:b+d=02i=01j=01wdijxdijb+\sum_{d=0}^{2} \sum_{i=0}^{1} \sum_{j=0}^{1} w_{d i j} x_{d i j}。卷积核的深度和感受视野的深度一样,都由输入数据来决定,长款可由自己来设定,数目也可以由自己来设定,一个卷积核依然对应一个feature map

注:“strid**e=1stride=1”表示在长和宽上的移动间隔都为1

激励层

激励层主要对卷积层的输出进行一个非线性映射,因为卷积层的计算还是一种线性计算。使用的激励函数一般为ReLu函数:

f(x)=max(x,0)f(x)=\max (x, 0)

卷积层和激励层通常合并在一起成为“卷积层”

池化层:

当输入经过卷积层时,若感受视野比较小,步长stride比较小,得到的feature map(特征图)还是比较大,可以通过池化层来对每一个feature map进行降维操作,输出的深度还是不变的,依然为feature map的个数

池化层也有一个“池化视野(filter)”来对feature map矩阵进行扫描,对“池化视野”中的矩阵值进行计算,一般有两种计算方法

  • Max pooling:取“池化视野”矩阵中的最大值
  • Average pooling:取“池化视野”矩阵中的平均值

扫描的过程同样会涉及的扫描布长stride,扫描方式同卷积层一样,先从左到右扫描,结束则向下移动布长大小,再从左到右。如下图示例所示:1562763459668

其中“池化视野”filter: 2 *2,步长stride:2.(注:“池化视野”为个人叫法)

最后可将3个24 *24的feature map下采样得到3个2 * 24 * 24的特征矩阵

1562763534831

归一化层

  • Batch Normalization

    Batch Normalization(批量归一化)实现了在神经网络层的中间进行预处理的操作,即在上一层的输入归一化处理后再进入网络的下一层,这样可有效地防止“梯度弥散”,加速网络训练

    Batch Normalization具体的算法如下图所示:1562763713958

    每次训练时,取 batch_size 大小的样本进行训练,在BN层中,将一个神经元看作一个特征,batch_size 个样本在某个特征维度会有 batch_size 个值,然后在每个神经元 xix_{i} 维度上的进行这些样本的均值和方差,通过公式得到 x^i\hat{x}_{i},再通过参数γ\gammaβ\beta进行线性映射得到每个神经元对应的输出 $$\boldsymbol{y}_{i}$$ 。在BN层中,可以看出每一个神经元维度上,都会有一个参数γ\gammaβ\beta,它们同权重w\boldsymbol{w}一样可以通过训练进行优化。

    在卷积神经网络中进行批量归一化时一般对 未进行ReLu激活的 feature map进行批量归一化,输出后再作为激励层的输入,可达到调整激励函数偏导的作用。

    一种做法是将 feature map 中的神经元作为特征维度,参数γ\gammaβ\beta 的数量和则等于 2×fmapwidth×fmaplength×fmapnum2 \times f m a p_{w i d t h} \times f m a p_{l e n g t h} \times f m a p_{n u m},这样做的话参数的数量会变得很多

    另一个做法是把 一个 feature map 看做一个特征维度,一个 feature map 上的神经元共享这个 feature map的 参数γ\gammaβ\beta ,参数γ\gammaβ\beta 的数量和则等于 2×fmapnum2 \times f m a p_{n u m},计算均值和方差则在batch_size个训练样本在每一个feature map维度上的均值和方差。

    注: fmapnumf m a p_{n u m}指的是一个样本的feature map数量,feature map跟神经元一样也有一定的排列顺序

    Batch Normalization 算法的训练过程和测试过程的区别:

    在训练过程中,我们每次都会将 batch_size 数目大小的训练样本 放入到CNN网络中进行训练,在BN层中自然可以得到计算输出所需要的 均值方差 ;

    而在测试过程中,我们往往只会向CNN网络中输入一个测试样本,这时在BN层计算的均值和方差会均为 0,因为只有一个样本输入,因此BN层的输入也会出现很大的问题,从而导致CNN网络输出的错误。所以在测试过程中,我们需要借助训练集中所有样本在BN层归一化时每个维度上的均值和方差,当然为了计算方便,我们可以在 batch_num 次训练过程中,将每一次在BN层归一化时每个维度上的均值和方差进行相加,最后再进行求一次均值即可。

切分层

在一些应用中,需要对图片进行切割,独立地对某一部分区域进行单独学习,这样可以对特定部分进行通过调整感受视野进行力度更大的学习

融合层

融合层可以对切分层进行融合,也可以对不同大小的卷积核学习到的特征进行融合

融合的方法有几种,一种是特征矩阵之间的拼接级联,另一种是特征矩阵进行运算(+,-,x,max,conv)

全连接层和输出层

全连接层主要对特征进行重新拟合,减少特征信息的丢失;输出层主要准备做好最后目标结果的输出

经典的卷积神经网络结构通常为

输入层 ----> (卷积层 + ----> 池化层?) + ----> 全连接层 + ----> 输出层

Tensorflow代码

主要的函数说明

卷积层:
tf.nn.conv2d(input, filter, strides, padding, use_cudnn_on_gpu=None, data_format=None, name=None)

参数说明:

  • data_format:表示输入的格式,有两种分别为:“NHWC”和“NCHW”,默认为“NHWC
    • data_format 设置为 “NHWC” 时,排列顺序为 [batch, height, width, channels];
    • data_format 设置为 “NCHW” 时,排列顺序为 [batch, channels, height, width]。

N表示这批图像有几张,H表示图像在竖直方向有多少像素,W表示水平方向像素数,C表示通道数(例如黑白图像的通道数 C = 1,而 RGB 彩色图像的通道数 C = 3)

两种格式的区别如下图所示:1562765065360

NCHW 中,C 排列在外层,每个通道内像素紧挨在一起,即 ‘RRRRRRGGGGGGBBBBBB’ 这种形式。

NHWC 格式,C 排列在最内层,多个通道对应空间位置的像素紧挨在一起,即 ‘RGBRGBRGBRGBRGBRGB’ 这种形式。

  • input:输入是一个4维格式的(图像)数据,数据的 shape 由 data_format 决定
  • filter:卷积核是一个4维格式的数据:shape表示为:[height,width,in_channels, out_channels],分别表示卷积核的高、宽、深度(与输入的in_channels应相同)、输出 feature map的个数(即卷积核的个数)。
  • strides:表示步长,一个长度为4的一维列表,每个元素跟data_format互相对应,表示在data——format每一维上的移动步长。当输入的默认格式为“NHWC”,则strides = [batch , in_height , in_width, in_channels],其中batch和in_channel要求一定为1,即只能在一个样本的一个通道上的特征图上进行移动,in_height,in_width表示卷积核在特征图的高度和宽度上移动的步长
  • padding:表示填充方式,“SAME”表示采用填充的方式,简单地理解以0填充边缘,当stride为1时,输入和输出的维度相同,“VALID”表示采用不填充的方式,多余的进行丢弃

池化层:

tf.nn.max_pool(value,ksize,strides,padding,data_format=’NHWC’,name=None)
tf.nn.avg_pool(…)

  • value:表示池化的输入,一个4维格式的数据,数据的shape由data_format决定,默认情况下shape为[batch, height, width, channels]
  • 其他参数与tf.nn.cov2d类型
  • ksize:表示池化窗口的大小,一个长度为4的一维列表,一般为[1, height, width, 1],因不想在batch和channels上做池化,则将其值设为1。

Batch Normalization层:

battch_normalization(x, mean, variance, offset, scale, variance_epsilon, name = “None”)

  • mean 和 variance通过tf.nn.moments来进行计算。batch_mean,batch_var = tf.nn.moments(x, axes = [0, 1, 2], keep_dims = True),注意axes的输入。对于以feature map为维度的全局归一化,若feature map的shape为[batcj, height, width, depth],则将axes赋值为[0, 1, 2]
  • x为输入的feature map四维数据,offset、scale为一维Tensor数据,shape等于feature map的深度depth

源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import torch
import torch.nn as nn
import torch.utils.data as Data
import torchvision
import matplotlib.pyplot as plt

torch.manual_seed(1) # reproducible

# Hyper Parameters
EPOCH = 1 # 训练整批数据多少次,为了节约时间,这里只训练一次
BATCH_SIZE = 50
LR = 0.001
DOWNLOAD_MNIST = True # 如果已经下载好了mnist数据就写上False

# Mnist手写数据
train_data = torchvision.datasets.MNIST (
root = './mnist', # 保存位置或提取位置
train=True, # this is training data
transform=torchvision.transforms.ToTensor(), # 转换PIL.Image or numpy.ndarray 成 torch.FloatTensor(C x H x W),训练的时候 normalize成[0.0, 1.0区间]
download=DOWNLOAD_MNIST, # 没下载就下载,下载了就不用再下了
)

test_data = torchvision.datasets.MNIST(
root='./mnist',
train=False
)

# 批训练50个samples,1 channel,28 *28 (50, 1, 28, 28)
train_loader = Data.DataLoader(
dataset=train_data,
batch_size=BATCH_SIZE,
shuffle=True,
)

# 为了节约时间,我们测试时只测试前2000个
test_x = torch.unsqueeze(test_data.test_data, dim=1).type(torch.FloatTensor)[:2000]/255.
test_y = test_data.test_labels[:2000]

"""
和以前一样,先用一个class来建立CNN模型,这个CNN整体流程是: 卷积(Conv2d) -> 激励函数(ReLU) -> 池化,向下采样(MaxPooling) -> 再来一遍 -> 展成多维的卷积成的特征图 -> 接入全连接层(Linear) -> 输出
"""

class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d( # input shape (1, 28, 28)
in_channels=1, # input height
out_channels=16, # n_filters
kernel_size=5, # filter size
stride=1, # filter movement/step
padding=2, # 如果想要 con2d 出来的图片长款没有变化,padding = (kernel_size - 1) / 2, 当stride = 1
), # output shape (16, 28, 28)
nn.ReLU(),
nn.MaxPool2d(kernel_size=2) # 在 2 * 2 空间里向下采样,output shape (16, 14, 14)
)

self.conv2 = nn.Sequential( # imput shape (16, 14, 14)
nn.Conv2d(16, 32, 5, 1, 2), # output shape (32, 14, 14)
nn.ReLU(), # activation
nn.MaxPool2d(kernel_size=2) # output shape (32, 7, 7)
)

self.out = nn.Linear(32 * 7 * 7, 10)

def forward(self,x):
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1) # 将输出展平
output = self.out(x)
return output

cnn = CNN()
# print(cnn)

"""
下面开始训练,将x y 都用 Variable 包起来,然后放入cnn 中计算output, 最后再计算误差
"""

optimizer = torch.optim.Adam(cnn.parameters(), lr=LR)
loss_func = nn.CrossEntropyLoss()

from matplotlib import cm
try: from sklearn.manifold import TSNE; HAS_SK = True
except: HSA_SK = False; print('Please install sklearn for layer visualization')

def plot_with_labels(lowDWeights, labels):
plt.cla()
X, Y = lowDWeights[:, 0], lowDWeights[:, 1]
for x, y, s in zip(X, Y, labels):
c = cm.rainbow(int(255 * s / 9))
plt.text(x, y, s, backgroundcolor=c, fontsize=9)
plt.xlim(X.min(), X.max()); plt.ylim(Y.min(), Y.max())
plt.title('Visualize last layer'); plt.show()
plt.pause(0.01)
plt.ion()

# training and testing
for epoch in range(EPOCH):
for step, (b_x, b_y) in enumerate(train_loader):
output = cnn(b_x)
loss = loss_func(output, b_y)
optimizer.zero_grad()
loss.backward()
optimizer.step()

if step % 50 == 0:
test_output, last_layer = cnn(test_x)
pred_y = torch.max(test_output, 1)[1].data.numpy()
accuracy = float((pred_y == test_y.data.numpy()).astype(int).sum()) / float(test_y.size(0))
print('Epoch: ', epoch, '| train loss: %.4f' % loss.data.numpy(), '| test accuracy: %.2f' % accuracy)
if HAS_SK:
# Visualization of trained flatten layer (T-SNE)
tsne = TSNE(perplexity=30, n_components=2, init='pca', n_iter=5000)
plot_only = 500
low_dim_embs = tsne.fit_transform(last_layer.data.numpy()[:plot_only, :])
labels = test_y.numpy()[:plot_only]
plot_with_labels(low_dim_embs, labels)
plt.ioff()



test_output = cnn(test_x[:10])
pred_y = torch.max(test_output, 1)[1].data.numpy().squeeze()
print(pred_y, 'prediction number')
print(test_y[:10].numpy(), 'real number')