卷积神经网络
起源
卷积神经网络(Convolutional Neural Networks,CNN)的发展可以追溯到20世纪50年代和60年代,对猫视觉皮层的研究。 根据感受野复杂性的不同,Hubel和Wiesel将皮层细胞分为简单细胞和复杂细胞。 简单细胞响应特定方向的边缘和条形。复杂细胞也会响应特定方向的边缘和条形,但它与简单细胞的不同之处在于,这些边和条形可以在场景中移动,并且细胞仍将响应。 例如,简单细胞可能仅响应图像底部的条形,而复杂细胞可能响应图像底部、中间或顶部的条形。复杂细胞的这种特性被称为"空间不变性"。 该研究解释说,简单细胞在小感受野中以确定的方向、位置和相位对小光点、边缘状刺激做出反应,而复杂细胞则具有大感受野。 复杂的细胞也往往表现出对位置和相位的不敏感。 Hubel和Wiesel得出的结论是,这些复杂细胞很可能接收来自几个简单细胞的输入。 简单的信息在前面提取,对简单信息提取然后得到高层次的抽象信息,视觉皮层利用这种层级结构处理信息。这样的层级结构启发了后来的卷积神经网络。
20世纪80年代,在Hubel和Wiesel在简单细胞和复杂细胞方面工作的启发下,福岛邦彦提出了 Neocognitron 模型。 它包含两种主要细胞类型,即以简单细胞命名的 S 细胞和以复杂细胞命名的 C 细胞。 S 细胞对应简单细胞或低阶超复杂细胞。C 细胞对应复杂细胞或更高级的超复杂细胞。 这两种细胞并非生物细胞,而是数学运算。 Neocognitron 由多个模块级联而成,每个模块由一层 S 细胞和一层 C 细胞组成。 S 细胞充当特征提取细胞。C 细胞接收多个 S 细胞的响应,然后使用非线性函数来计算 S 细胞的响应。 在代表初级视觉皮层基本计算的一层简单细胞和复杂细胞之后,Neocognitron 重复这个过程。经过多次重复,创建出一个层级结构的模型。 Neocognitron 通过无师自通的方式完成自组织。 即反复向它呈现一组刺激模式,它就会逐渐获得识别这些模式的能力。 自组织完成后,网络获得类似于Hubel和Wiesel(1962,1965)提出的视觉神经系统层次模型的结构。 Neocognitron 模型中的 S 细胞层可以看作是卷积层。 S 细胞的权重是可学习的,并且每个 S 细胞都会选择性地对输入图像中的特定视觉特征做出反应。这与卷积层中的卷积核非常相似。 Neocognitron 模型中的 C 细胞层可以看作是池化层。 C 细胞的响应是来自同一平面但位于不同位置的多个 S 细胞的非线性函数。这与池化层中的池化操作非常相似。 福岛邦彦提出的 Neocognitron 能够不受位置移动或刺激模式形状小变形的影响,识别刺激模式。
1989 年,贝尔实验室的 Yann LeCun 等人将反向传播算法应用于手写邮政编码识别。 学习网络直接通过图像而不是特征向量来进行学习的,从而证明了反向传播网络处理大量低级信息的能力。 受约束的反向传播是该算法成功的关键,它大大减少了自由参数的数量,提高了神经网络的泛化能力。 消除了对手动特征工程的需求,通过卷积层、下采样和全连接层直接处理像素图像 他结合了一个由反向传播算法训练的卷积神经网络来读取手写数字,并成功地将其应用于识别美国邮政服务提供的手写邮政编码号码。这就是后来被称为LeNet的原型。 1998年,Yann LeCun 等人在 1998 年发表的论文《Gradient-Based Learning Applied to Document Recognition》中提出了 LeNet-5 模型,该模型在手写数字识别任务上取得了非常好的效果。 LeNet-5 标志着卷积神经网络的诞生,并概述了其核心组成部分。 CNN 随着深度学习的兴起而得到了广泛的应用。 2012年,AlexNet 模型在 ImageNet 竞赛中取得了冠军,当时用反向传播训练的 8 层网络在 ImageNet 挑战赛中远远超过了最先进的性能。 此后,CNN 在各个领域都取得了巨大的成功。 此后,卷积神经网络在各个领域广泛应用,并在很多问题上都取得了最好的性能。
计算
卷积神经网络(Convolutional Neural Networks, CNNs)作为深度学习的一种重要范式,其设计理念源自于对生物视觉皮层处理机制的模拟。 CNNs 在图像识别、视频分析、自然语言处理等众多领域展现出了卓越的性能。 卷积层是CNNs的核心组成部分,通过在输入数据上应用一系列可学习的滤波器(或称卷积核),实现了对局部特征的自动提取。 具体而言,卷积操作通过计算滤波器与输入数据的元素对应乘积之和,得到特征图,卷积运算如图所示。

卷积运算
激活函数通常用于卷积层和全连接层,是卷积层的最后一个组成部分。 激活函数的主要作用是完成数据的非线性变换,解决线性模型的表达、分类能力不足的问题。 卷积本质上是线性操作,因为它们是通过卷积核(权重)和特征图的元素相乘然后求和来实现的。 没有激活函数,无论有多少个卷积层,整个网络仍然是线性的,这极大地限制了网络的表达能力。
如果网络中全部是线性变换,则多层网络可以通过矩阵变换,退化为单层神经网络。 所以激活函数的存在,使得神经网络的“多层”有了实际的意义,使网络更加强大,增加网络的能力,使它可以学习复杂的事物,复杂的数据,以及表示输入输出之间非线性的复杂的任意函数映射。 使用梯度下降法训练深层网络时。如果神经网络的所有层都是线性的,那么梯度很容易随着层次的增加而变得非常小,导致网络无法学习。 使用激活函数,特别是如ReLU这样的函数,可以减缓梯度消失的问题,使得网络能够构建更深层的模型。

激活函数
池化层(Pooling Layer)通常位于卷积层之后,其目的在于降低特征图的空间维度,从而减少后续计算的复杂度,并提升模型对图片的位移、缩放和旋转保持不变性。 池化操作主要有最大池化(Max Pooling)和平均池化(Average Pooling)两种。

平均池化

最大池化
全连接层(Fully Connected Layer)位于CNNs的末端。 全连接层的每个神经元与前一层的所有神经元相连,每个连接都有一个权重用于调节信息传递的强度,并且每个神经元还有一个偏置项。 全连接层的主要功能是将前向传播过程中获得的高阶特征进行综合,以完成分类、回归等最终任务。 如果说卷积层、池化层和激活函数等操作是将原始数据映射到隐层特征空间的话,全连接层则起到将学到的特征表示映射到样本的标记空间的作用。
卷积取的是局部特征,全连接就是把以前的局部特征重新通过权值矩阵组装成完整的图。 假设你是一只小蚂蚁,你的任务是找小面包。你的视野还比较窄,只能看到很小一片区域。 当你找到一片小面包之后,你不知道你找到的是不是全部的小面包,所以你们全部的蚂蚁开了个会,把所有的小面包都拿出来分享了。全连接层就是这个蚂蚁大会。

全连接层
卷积网络在形式上有一点点像咱们正在召开的“人民代表大会”。 卷积核的个数相当于候选人,图像中不同的特征会激活不同的“候选人”(卷积核)。 池化层(仅指最大池化)起着类似于“合票”的作用,不同特征在对不同的“候选人”有着各自的喜好。 全连接相当于是“代表普选”。 所有被各个区域选出的代表,对最终结果进行“投票”,全连接保证了receiptive field 是整个图像,既图像中各个部分(所谓所有代表),都有对最终结果影响的权利。
实现
1 import numpy as np
2 from CNN.losses import mean_squared_loss
3 import pandas as pd
4
5 class Convolution():
6 """卷积层"""
7 # 权重 偏差 步幅 填充
8 def __init__(self,weight,bias,stride = 1,padding = 0):
9
10 self.weight = weight
11 self.bias = bias
12 self.stride = stride
13 self.padding = padding
14
15 self.x = None
16
17 def _convolution_(self,img,weight,bias = False):
18
19 # 输入数据形状(样本数 通道 宽度 高度)
20 batch_size, in_channel, in_height, in_width = img.shape
21
22 # 卷积核形状(输入通道 输出通道 宽度 高度)
23 in_channel, f_out_channel, f_height, f_width = weight.shape
24
25 # 输出数据的高和宽
26 out_height = 1 + int((in_height + 2 * self.padding - f_height) / self.stride)
27 out_width = 1 + int((in_width + 2 * self.padding - f_width) / self.stride)
28
29 # 卷积运算的输出
30 out = np.zeros((batch_size,f_out_channel,out_height,out_width))
31
32 # 输出的形状 batch_size, f_out_channel, out_height, out_width
33 for b in np.arange(batch_size):
34 for c in np.arange(f_out_channel):
35 for h in np.arange(out_height):
36 for w in np.arange(out_width):
37 if bias == True:
38 out[b,c,h // self.stride,w // self.stride] = np.sum(img[b,:,h:h + f_height,w:w + f_width] *
39 weight[:,c]) + self.bias[c]
40 elif bias == False:
41 out[b, c, h // self.stride, w // self.stride] = np.sum(
42 img[b, :, h:h + f_height, w:w + f_width] * weight[:, c])
43 return out
44
45 def con_forward(self, x):
46 """前向传播 输出卷积结果"""
47
48 # 填充图像
49 img = np.pad(x, [(0, 0), (0, 0), (self.padding, self.padding), (self.padding, self.padding)], 'constant')
50
51 # 计算卷积 加上偏置
52 out = self._convolution_(img,self.weight,bias = True)
53
54 self.x = x
55
56 return out
57
58 def con_backward(self,dout):
59 """反向传播"""
60 # 卷积核形状(输入通道 输出通道 宽度 高度)
61 in_channel, f_out_channel, f_height, f_width = self.weight.shape
62
63 # 卷积核翻转180°
64 weight_180 = np.flip(self.weight,(2,3))
65
66 # 交换通道
67 weight_180 = np.swapaxes(weight_180,0,1)
68
69 # 填充
70 pad_out = np.pad(dout, [(0, 0), (0, 0), (f_height-1,f_height-1), (f_width-1,f_width-1)], 'constant')
71
72 # 前一层的δ
73 dout_last = self._convolution_(pad_out,weight_180,bias=False)
74
75 # 交换 输入x的通道 变为 channel batch H W
76 x = np.swapaxes(self.x,0,1)
77
78 # 权重的梯度
79 dw = self._convolution_(x,dout)
80
81 # 偏置的梯度
82 db = np.sum(np.sum(np.sum(dout, axis=-1), axis=-1),axis=0)
83
84 batch = self.x.shape[0]
85
86
87 return dw/batch,db/batch,dout_last
88
89 class Pooling():
90 """池化层"""
91 def __init__(self,pool,stride = 2,padding = 0):
92
93 # 池化窗口的高和宽
94 self.pool_h = pool[0]
95 self.pool_w = pool[1]
96 self.padding = padding
97 self.stride = stride
98
99 self.x = None
100 self.list = []
101
102 def pool_forward(self,x):
103 """前向传播"""
104
105 # 输入数据的形状
106 batch_size,channel,height,width = x.shape
107
108 # 填充
109 img = np.pad(x, [(0, 0), (0, 0), (self.padding, self.padding), (self.padding, self.padding)], 'constant')
110 self.x = img
111
112 # 输出数据的高和宽
113 out_h = (height - self.pool_h) // self.stride + 1
114 out_w = (width - self.pool_w) // self.stride + 1
115
116 # 输出数据
117 pool_out = np.zeros((batch_size,channel,out_h,out_w))
118
119 for b in np.arange(batch_size):
120 for c in np.arange(channel):
121 for h in np.arange(out_h):
122 for w in np.arange(out_w):
123 pool_out[b,c,h,w] = np.max(img[b,c,
124 self.stride * h : self.stride * h + self.pool_h,
125 self.stride * w : self.stride * w + self.pool_w])
126
127 return pool_out
128
129 def pool_backward(self,dout):
130
131 # 前一层数据形状
132 batch_size, channel, height, width = self.x.shape
133
134 # 池化层输出数据形状
135 _,_,out_h,out_w = dout.shape
136
137 # 前一层的δ
138 dout_last = np.zeros((batch_size,channel,height,width))
139
140 for b in np.arange(batch_size):
141 for c in np.arange(channel):
142 for h in np.arange(out_h):
143 for w in np.arange(out_w):
144 max_index = np.argmax(self.x[b,c,
145 self.stride * h: self.stride * h + self.pool_h,
146 self.stride * w: self.stride * w + self.pool_w])
147
148 h_index = self.stride * h + max_index // self.pool_w
149 w_index = self.stride * w + max_index % self.pool_w
150
151 dout_last[b,c,h_index,w_index] += dout[b,c,h,w]
152
153 return dout_last
154
155 class FullConnect():
156 """全连接层"""
157 def __init__(self,input_nodes,hidden_nodes,output_nodes,learning_rate):
158
159 #初始化输入数据:输入层神经元个数、隐藏层神经元个数、输出层神经元个数、学习率
160 self.input = input_nodes
161 self.hidden = hidden_nodes
162 self.output = output_nodes
163 self.lr = learning_rate
164
165 # 输入层和隐藏层之间的权重 #高斯分布的均值 #标准差 #大小
166 self.weight_i_h = np.random.normal(0.0, pow(self.hidden,- 0.5),(self.input,self.hidden))
167 # 隐藏层和输出层之间的权重
168 self.weight_h_o = np.random.normal(0.0, pow(self.output,- 0.5),(self.hidden,self.output))
169 # sigmoid激活函数
170 self.sigmoid = lambda x: 1.0/(1 + np.exp(-x*1.0))
171
172 def fc_forward(self, input_data):
173 self.input_data = input_data
174 #计算隐藏层输入
175 hidden_input = np.dot(self.input_data, self.weight_i_h)
176 #计算隐藏层输出
177 self.hidden_output = self.sigmoid(hidden_input)
178 #计算输出层输入
179 final_input = np.dot(self.hidden_output, self.weight_h_o)
180 #计算输出层输出
181 self.final_output = self.sigmoid(final_input)
182
183 return self.final_output
184
185 def fc_backward(self,target):
186
187 #计算在输出层的损失
188 delta_h_o = (target -self.final_output) * self.final_output * (1-self.final_output)
189 #计算在隐藏层的损失
190 delta_i_h = delta_h_o.dot(self.weight_h_o.T) * self.hidden_output * (1-self.hidden_output)
191 #计算输入层(展平层flatten)的损失
192 delta_flatten = delta_i_h.dot(self.weight_i_h.T)
193 #隐藏层_输出层权重更新
194 delta_weight_h_o = self.lr * self.hidden_output.T.dot(delta_h_o)
195 self.weight_h_o += delta_weight_h_o
196 #输入层_隐藏层权重更新
197 delta_weight_i_h = self.lr * self.input_data.T.dot(delta_i_h)
198 self.weight_i_h += delta_weight_i_h
199
200 return delta_flatten
201
202 class Activation():
203 """激活层"""
204
205 def relu_forward(self,input):
206
207 self.input = input
208 return np.maximum(0,input)
209
210 def relu_backward(self,next_dout):
211
212 dout = np.where(np.greater(self.input, 0), next_dout, 0)
213 return dout
References
[1] 必读论文 | 卷积神经网络百篇经典论文推荐
[2] Rachel Draeos, The History of Convolutional Neural Networks
[3] Lindsay, Grace W. "Convolutional neural networks as a model of the visual system: Past, present, and future." Journal of cognitive neuroscience 33.10 (2021): 2017-2031.
[4] Bhiksha Raj,Convolutiohnal Networks II
[5] Lunarnai, CNN简史
[6] Fukushima, Kunihiko. "Neocognitron: A self-organizing neural network model for a mechanism of pattern recognition unaffected by shift in position." Biological cybernetics 36.4 (1980): 193-202.
[7] Fukushima, Kunihiko. "Artificial vision by multi-layered neural networks: Neocognitron and its advances." Neural networks 37 (2013): 103-119.
[8] 卷积神经网络各层的作用
[9] Paul Xiong, 卷积神经网络初学者指南
[10] NIC Lab, 深度学习发展浅谈