CNN是现代的机器深度学习一个很核心的内容,就假如说咱们做图像分类、图像分割,图像的切分等等。
其实这些过程就是你让计算机能够自动识别,不仅能够识别图像里有什么,还能识别图像里这些东西分别是在什么地方。这种复杂操作其实都是基于啊CNN的变体。要给计算机有识别图像的能力。
再比方说大无人驾驶汽车,它要识别行人在哪里。
再比如安防的摄像头,要能够检测出来我们人在哪里。
这些事情背后都是计算机视觉的问题。
大概一九五几年、六几年的时候,哈佛大学曾经做过一个研究,给猫的大脑上装了一些电极,让这个猫去看前面的一个幻灯片,然后通过切换幻灯片的内容,然后观察猫的大脑哪些地方活跃。
就发现两个特点,第一个它有一种一层一层的特性,比方说我换了颜色,它固定的就这几层会活跃,离眼睛远的地方会活跃。如果换了线条,颜色没变,会是另外的一层区域会活跃,不同层其实对于不同的特定变化是不一样的。
第二个发现,越靠近眼睛的地方,越低级的层次的变化会越明显,比如线条颜色。眼睛越远的距离,线条和颜色没变,但是眼睛变大了或者变小了,那么这些地方它会更明显。
也就是说,第一个它是有分层的,第二个,它不同的这个层的抽象性是不一样的,对于什么东西的感受力是不一样的。
沿着这个思路,人们当时就提出来了一些方法。当时人们做计算机视觉,主流不是机器学习。但是人们提出来一个一个这样的filter:
filter = np.array([
[1, 0, -1],
[1, 0, -1],
[1, 0, -1]
])
这样的filter是人刻意的,主观的提出来的。他们把这个filter去应用到一个一个的图像上。
比方说我们的图像是a b c d e f g h i j k l
,然后按4*4的矩阵相乘,再加起来,比如,这样就得到了一个新的内容。大家把这个操作就叫做卷积操作。
看个示例:
import numpy as np
image = np.array([
[10, 10, 10, -3, -3, -3],
[10, 10, 10, -3, -3, -3],
[10, 10, 10, -3, -3, -3],
[10, 10, 10, -3, -3, -3],
[10, 10, 10, -3, -3, -3],
[10, 10, 10, -3, -3, -3],
])
plt.imshow(image)
plt.show()
我们可以看到,这个矩阵的前三列全是10,后两列都是0,最后生成的图像有一个明显的分界,伴随着两个不同的颜色。
我们现在给这个图像矩阵加上一个filter, 然后按上面的方法进行操作:
那左上角的3*3的小矩阵的运算结果就是0。
那同理,我们以此往后算,第二个结果是39, 第三个结果是39.... 大家后面可以自行计算一下,最后的计算结果就是:
[[0, 39, 39, 0],
[0, 39, 39, 0],
[0, 39, 39, 0]]
我们可以看出来,当分割的小矩阵内数据相同的时候,值为0,如果说矩阵内的这个部分图像差距不是很大,那它也是近乎接近于0,意味着差别很小。如果说分割的这个小矩阵左右两边是相反数的时候,两边的差别是最大的,不管最后相加的值是正的还是负的,绝对值下应该是最大的。这个地方其实是图像竖着的边缘。
那如果我们将filter改一下,改成下面这样:
[[1, 1, 1],
[0, 0, 0],
[-1, -1, -1]]
如果是这样,计算的结果就是图像横向的边缘的绝对值最大。
基于这种原理,我们就可以找到图像所有竖向和横向的边沿,给它拿出来。
这整个的一个过程,就叫做卷积: convolution。convolution就是两个东西之间互相起作用。最早是出现在信号处理上,两个信号把它做一个合并。
卷积的操作是为了干什么呢?卷积的操作是用来提取图片的某种特征,抓取图片特征。在上个世纪后期,计算机视觉的老科学家们提出了大量的kernel,当时叫做算子,现在叫做卷积核。
卷积的操作就是给定一个图片,然后给定一个卷积核,和卷积核一样大小的窗口里边的每个值相乘,相乘之后再做相加。
假如咱们有一张图片,一般来说,咱们现实生活中图片往往是三维,通常是红绿蓝(RGB),然后我们让这张图片和这个filter去做相乘的操作。
这三个层里面每一层都会和filter做一个相乘的操作,咱们就假设这三个层分别为:
[[a11, a12, a13],[a21, a22, a23],[a31, a32, a33]],
[[b11, b12, b13],[b21, b22, b23],[b31, b32, b33]]
[[c11, c12, c13],[c21, c22, c23],[c31, c32, c33]]
然后再假设filter为:
[[f11, f12, f13], [f21, f22, f23], [f31, f32, f33]]
那这个filter会分别和这三个层进行卷积操作,产生的卷积结果为v1, v2, v3, 然后这三个结果再进行相加,最后会产生一个新的层。
我们来看一下下面这张图:
这张图显示的是一层的情况,一个filter大小的矩阵被卷积成了一个点,然后这个操作不只是针对一层的,而是对整个一个纵向体积内的所有层都做这样一个操作:
途中最底下是我们的图片的RGB分层,再经过和filter相乘之后向上会卷积成一个点,那向上之后的Map1, Map2,... 原因是每一层都是一个不同的filter计算的结果,这里存在很多个filter, 然后分别计算产生了这样一个叠加层。
再做下一次运算的时候也是一样,这些Map的纵向上经过和filter运算依然会被卷积成一个点。
就着上面那个简单的图形,咱们来做个演示:
def conv(image, filter):
h = filter.shape[0]
w = filter.shape[1]
for i in range(image.shape[0]):
for j in range(image.shape[1]):
window = image[i: i+h, j: j+w]
print(window)
filter = np.array([
[1, 0, -1],
[1, 0, -1],
[1, 0, -1]
])
conv(image, filter)
---
[[101010]
[101010]
[101010]]
...
[[-3]
[-3]
[-3]]
...
[[10-3-3]]
[[-3-3-3]]
[[-3-3]]
[[-3]]
输入一个图片的数据, 拿到filter的高宽,然后让filter沿着图片从上到下,从左到右移动。
我们打印结果能看到,运行到中间的时候会出现一串的[[-3], [-3], [-3]]。因为i会一直运行边上,那么如果要做卷积的话,大小要和filter一直一样,所以咱们在这里需要给他减去一个filter。就是不要运行后边这几个。
...
for i in range(image.shape[0] - h+1):
for j in range(image.shape[1] - w+1):
...
这样就可以了。
我们每一次其实就是从左到右,从上到下裁剪出来一个一个的window。
我们让这个window和filter相乘后再相加,我们可以得到什么结果?
for i ...:
for j ...
...
result = np.sum(filter * window)
print(result)
---
0
39
...
39
0
就是计算卷积的结果。
那我们可以将其改成矩阵的形式, 然后咱们打印出来看看是个啥:
def conv(image, filter):
r_height = image.shape[0] - h+1
r_width = image.shape[1] - w+1
result = np.zeros(shape=(r_height, r_width))
for i ...:
for j ...:
result[i][j] = np.sum(filter * window)
return result
result = conv(image, filter)
plt.imshow(result)
plt.show()
---
array([[ 0., 39., 39., 0.],
[ 0., 39., 39., 0.],
[ 0., 39., 39., 0.],
[ 0., 39., 39., 0.]])
那变成这样的原因是因为原来的图像中间有一个边缘,现在这张图显示的是图片边缘的部分被高亮。
这样一张图片可能并不太能理解,我拿我的头像来做这个示例好了:
myself = Image.open('./assets/chaheng.jpg').convert('L')
...
plt.imshow(result, cmap='gray')
为了更明显一点,我将图像改成灰度显示。
我们可以看到卷积之后的效果,明显边缘都被显示出来了。但是我们也注意到了,竖向的边缘都很明显,但是横向的边缘并不清楚。我们再来对横向进行一下卷积, 我们先要增加一个处理多个filter的方法,将原来的conv方法改为single_conv, 表示处理单个:
def single_conv(...):
...
def conv(image, filters):
results = [single_conv(image, f) for f in filters]
return results
然后我们的调用需要改一下传递的参数:
results = conv(image, [h_filter, w_filter])
既然要传递两个filter, 那我们就需要再定义一个横向的filter,然后一起传进去:
# 原来的filter
h_filter = np.array([...])
# 新定义的横向filter
w_filter = np.array([
[1, 1, 1],
[0, 0, 0],
[-1, -1, -1]
])
接着我们将原图,竖向的卷积结果和横向的卷积结果都打印出来:
plt.subplot(1, 3, 1)
plt.imshow(image)
plt.subplot(1, 3, 2)
plt.imshow(results[0])
plt.subplot(1, 3, 3)
plt.imshow(results[1])
plt.show()
原图变成这个颜色的原因是我在PIL读取图像的时候,将其转为了灰度。我们可以看到第二张图片和第三张图明显在边缘上的区别,一个像是灯光从左边打过来的,一个像是灯光从上面打下来的。
中间和右边这个,其实都是把边缘提出来了。因为卷积核的不同,中间这个图把竖着的边缘明显提取的比较准确,右边的把横向的提取的比较准确。
这也是为什么我们之前看得那张图里会有那么多的Map:
它的每一层都是一个不同的filter提取出来的,有这么多filter的原因则是每一个filter提取出来的特征都是不一样的。
我们来看我们刚才定义的方法:
def single(image, filter):
...
我们把输入卷积的时候的image这个参数叫做input channel
。那在此时此刻,我们这个图像如果是RGB的,它就是三维的,那么input channel
就等于3。
filters
的个数,就叫做output channel
。原因就在于,有多少个filter
,那我们的results就有多厚。比如说我们有4个filter
, 那输出的result就有四层。然后可以接着对results继续应用filter
做卷积,那在这一轮的input channel
就等于一次的output channel
, 也就是4。
这个,就是卷积的原理。