先引入一个二维卷积:
表示f的索引(a,b)和g的索引(i-a,j-b)的对应和相加。
对于一个二维数组A,其维度为w,h,使用一个卷积核为m,n,在不加以填充的前提条件下,输出维度为(w-m+1, h-n+1)。
这样一个二维卷积计算为:
def conv(x, k):
H, W = k.shape
output = np.zeros((x.shape[0]-H+1, x.shape[1]-W+1))
for i in range(output.shape[0]):
for j in range(output.shape[1]):
output[i, j] = (X[i:i+H, j:j+W] * k).sum()
return output
卷积层也被称为特征映射,而某一层的任意元素x,受到前面几层的影响所包含的范围就是感受野。
比如给定一个矩阵大小为9*9,第一层卷积核大小为3*3,第二层也为3*3,则输出后的第一层矩阵为A,大小为(7*7),一共四十九个元素,每个元素受到前面3*3的作用,因此这个第一层的感受野为3*3,第二层生成的矩阵大小为(5*5),它在前一层的感受野为3*3,原始图像的感受野为5*5,因此如果我们需要更大的感受野,可以加深卷积层的深度。
在前面的卷积过程中,明显会丢失一些边缘信息,导致输出矩阵的维度减小了。为了维持输出矩阵的维度,可以给矩阵的边缘填充数行或者数列。
假设卷积核的大小为(h,w),二维矩阵的大小(m,n)那么我们需要填充的高度和宽度为h-1和w-1,经过卷积层之后,维度将会保持不变。一般情况下卷积核的大小为奇数,这样容易计算,上下侧和左右侧都天骄(h-1)/2和(w-1)/2,如果为偶数,则上册添加(h-1)/2行,数字向上取整,底部添加(h-1)/2,数字向下取值。左右也是如此计算。
假设二维矩阵大小和卷积核大小还是(m,n)和(h,w);ph=h-1,pw=w-1
在窗口移动的过程,水平移动的步长为sw,垂直移动的步长为sh,则输出的形状为(m-h+ph+sh)/sh,(n-w+pw+sw)/sw
总所周知,图像一般是包含有三个通达的,因此他们一般是三维张量(3,h,w)
当输入包含多个通道的时候,需要构造一个具有与输入数据相同输入通道的卷积核,假设图片是三维张量(3,h,w),则卷积核的大小为(3,h,w),卷积核的维度与图像的维度相卷积,然后累加,最后只输出一个二维矩阵,只有一个输出通道就是如此。
def conv(x, k):
H, W = k.shape
output = np.zeros((x.shape[0]-H+1, x.shape[1]-W+1))
for i in range(output.shape[0]):
for j in range(output.shape[1]):
output[i, j] = (x[i:i+H, j:j+W] * k).sum()
return output
def mul_cocv(X, K):
t = X.shape[0]
result = np.zeros((X.shape[1]-K.shape[1]+1, X.shape[2]-K.shape[2]+1))
for i in range(t):
result += conv(X[i], K[i])
return result
上面讨论的输出都是单通道,再卷积神经网络中会用到多通道,卷积核大小为(output,input,h,w),将单通道扩展到多通道,就是将每一个卷积核通道与对应的多维矩阵进行卷积。
def conv(x, k):
H, W = k.shape
output = np.zeros((x.shape[0]-H+1, x.shape[1]-W+1))
for i in range(output.shape[0]):
for j in range(output.shape[1]):
output[i, j] = (x[i:i+H, j:j+W] * k).sum()
return output
def mul_cocv(X, K):
t = X.shape[0]
result = np.zeros((X.shape[1]-K.shape[1]+1, X.shape[2]-K.shape[2]+1))
for i in range(t):
result += conv(X[i], K[i])
return result
#多通道输出
def mul_conv_out(X, K):
output_shape = K.shape[0]
result = []
for i in range(output_shape):
result.append(mul_cocv(X,K[i]))
return np.array(result)
M*N的卷积层可以学习到相邻像素的相关特征,1*1的卷积核的计算仅仅是将同一个像素的通道连接到了一起,做了一个线性变换,所以1*1的卷积核相当于贯穿输入维度的全连接层。
PS:上面的所有运算都没有使用激活函数
池化层的作用是降低隐藏表示的空间分辨率,聚合信息,同时还能产生平移不变性。简单来讲,就是降低卷积层对 位置的敏感性,同时降低对空间降采样的敏感性。池化层没有太复杂的运算,只是把窗口的数据进行简单的计算,池化层一般分为两种,一个是最大池化层,另一个是平均池化层。
1.最大池化层和平均池化层
池化层的窗口和单通道的卷积层窗口计算规律一致,也可以调整步长和填充,默认情况下步幅和窗口大小一致。下面的代码时池化层的计算方法
def pool2d(X, pool_size, mode="max"):
h, w = pool_size
output = np.zeros((X.shape[0]-h+1, X.shape[1]-w+1))
for i in range(output.shape[0]):
for j in range(output.shape[1]):
if mode=="max":
output[i,j] = X[i:i+h, j:j+w].max()
elif mode == "avg":
output[i,j] = X[i:i+h, j:j+w].mean()
return output
这里没有写步幅和填充怎么实现,但是也不是很难,就是稍微有点麻烦,在矩阵周围填充,还有移动过程的步长设置一下就行了。
2.多个通道
在处理多个通道的时候,池化层在每个通道上都进行了一次计算。