在CNN模型,最常见的是二维的卷积层,我们也从这个方面开始介绍。
在二维的卷积层中,一个二维输入数组和一个二维核数组通过互相关运算输出一个二维数组。举一个具体的例子来说:
这里定义原始的二维矩阵为33的矩阵,定义卷积核为22,通过卷积核在原始矩阵上的滑动来进行互运算,以蓝色的部分为例:
0 ∗ 0 + 1 ∗ 1 + 3 ∗ 2 + 4 ∗ 3 = 19 0*0+1*1+3*2+4*3=19 0∗0+1∗1+3∗2+4∗3=19
在二维的互运算中,卷积核从原始的输入矩阵从左上方开始,按从左向右,从上往下的顺序,依次在输入数组上滑动。当卷积核滑动到某一个位置的时候,窗口输入的子矩阵和卷积核按照元素相乘并求和,得到输出矩阵对应位置的元素。
我们手动的来实现一下这种计算方式:
#encoding=utf-8
import torch
import torch.nn as nn
def corr2d(X,k):
'''
:param X: 原始输入矩阵
:param k: 卷积核
:return:
'''
h,w = k.shape # 卷积核的维度
Y = torch.zeros((X.shape[0] - h + 1,X.shape[1] - w + 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
if __name__ == '__main__':
X = torch.tensor([[0,1,2],[3,4,5],[6,7,8]])
k = torch.tensor([[0,1],[2,3]])
res = corr2d(X,k)
print(res)
进一步,二维的卷积层将输入和卷积核做互相关运算,并加上一个标量偏差来得到输出。卷积层模型参数包括卷积核和标量偏差。在训练过程中,首先对卷积核内部的参数进行随机的初始化,然后在迭代的过程中对卷积的参数进行调参优化。我们自定义一个卷积层:
class Conv2D(nn.Module):
def __init__(self,kernel_size):
super(Conv2D,self).__init__()
self.weight = nn.Parameter(torch.randn(kernel_size))
self.bias = nn.Parameter(torch.randn(1))
def forward(self,x):
return corr2d(X,self.weight) + self.bias
基于卷积层,我们来实现一个简单的应用:检测图像中物体的边缘,即找到像素变化的位置。首先,构造出一个6*8的图像,它中间4列为黑(用0表示),其余的位置为白(用1表示)。
#encoding=utf-8
import torch
import torch.nn as nn
def corr2d(X,k):
'''
:param X: 原始输入矩阵
:param k: 卷积核
:return:
'''
h,w = k.shape # 卷积核的维度
Y = torch.zeros((X.shape[0] - h + 1,X.shape[1] - w + 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
class Conv2D(nn.Module):
def __init__(self,kernel_size):
super(Conv2D,self).__init__()
self.weight = nn.Parameter(torch.randn(kernel_size))
self.bias = nn.Parameter(torch.randn(1))
def forward(self,x):
return corr2d(X,self.weight) + self.bias
# 构建图片
X = torch.ones(6,8)
X[:,2:6] = 0
#构建卷积核
K = torch.tensor([[1,-1]],dtype=torch.float32)
Y = corr2d(X,K)
#设置超参数
step = 20
lr = 0.01
conv2d = Conv2D(kernel_size=(1,2))
if __name__ == '__main__':
for i in range(step):
Y_hat = conv2d(X)
loss_vlaue = ((Y_hat- Y) ** 2 ).sum()
loss_vlaue.backward()
#梯度下降
conv2d.weight.data -= lr * conv2d.weight.grad
conv2d.bias.data -= lr * conv2d.bias.grad
#梯度清0
conv2d.weight.grad.fill_(0)
conv2d.bias.grad.fill_(0)
if(i+1) % 5 == 0:
print("Step %d. loss %.3f" %(i+1,loss_vlaue.item()))
二维的卷积层输出的二维数组可以看做是输入在某一个向量空间中的一个表征,也称为特征图。影响元素x的前向计算的所有可能输入区域(也就是卷积核划过的部分,可能大于原始矩阵X)称为X的感受野。以下图为例:
其中输入层的感受野为9,当我们使用卷积核在输出上进行卷积的时候,感受野为4。通过不断的增加卷积层的深度,可以不断地扩大感受野。
Padding是指在输入高和宽两侧填充元素(一般是0元素),依然以上面的图为例子,经过一个padding_size=1之后,结果如下图所示:
如果不进行padding,则获得的结果矩阵为:
( n k − k h + 1 ) ∗ ( n k − k w + 1 ) (n_k-k_h+1) *(n_k-k_w+1) (nk−kh+1)∗(nk−kw+1)
如果使用了padding进行填充,则获得的结果矩阵为:
( n k − k h + p h + 1 ) ∗ ( n k − k w + p w + 1 ) (n_k-k_h+p_h+1) *(n_k-k_w+p_w+1) (nk−kh+ph+1)∗(nk−kw+pw+1)
其中 ( n k . n w ) (n_k.n_w) (nk.nw)为原始矩阵的维度, ( k h , k w ) (k_h,k_w) (kh,kw)表示卷积核的维度, ( p h , p w ) (p_h,p_w) (ph,pw)为padding的维度。这里我们举一个经过padding之后的计算实例
#encoding=utf-8
import torch
import torch.nn as nn
def corr2d(X,k):
'''
:param X: 原始输入矩阵
:param k: 卷积核
:return:
'''
h,w = k.shape # 卷积核的维度
Y = torch.zeros((X.shape[0] - h + 1,X.shape[1] - w + 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
class Conv2D(nn.Module):
def __init__(self,kernel_size):
super(Conv2D,self).__init__()
self.weight = nn.Parameter(torch.randn(kernel_size))
self.bias = nn.Parameter(torch.randn(1))
def forward(self,x):
return corr2d(X,self.weight) + self.bias
def comp_conv2d(conv2d,X):
# 定义batch 和 channel的数量
X = X.view((1,1) + X.shape)
print(X.shape)
Y = conv2d(X)
return Y.view(Y.shape[2:])
if __name__ == '__main__':
cov2d = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3,padding=1)
conv2d = nn.Conv2d(in_channels=1,out_channels=1,kernel_size=(5,3),padding=(2,1))
X = torch.rand(8,8)
sha = comp_conv2d(conv2d=conv2d,X=X).shape
print(sha)
这里我们使用到了nn.Conv2d(),这是torch内部自带的二维卷积层,其常用参数情况如下所示:
对于卷积还可以使用nn.Conv1d(一维卷积),nn.Conv3d(三维卷积),空间维度不同,单操作方式相同,参数也类似,这里就不在赘述了。
在这一节的最后,我们来关注一下步长的问题,步长决定了卷积核每次滑动的距离,我们在使用nn自带的卷积层的时候,在初始化的过程中,通过定义stride来进行确定,默认值为1。
这里我们不介绍概念,只是介绍一些在实现的时候,需要注意的问题:
def corr2d(X,k):
'''
:param X: 原始输入矩阵
:param k: 卷积核
:return:
'''
h,w = k.shape # 卷积核的维度
Y = torch.zeros((X.shape[0] - h + 1,X.shape[1] - w + 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
def corr2d_multi_in(X,K):
res = corr2d(X[0,:,:],K[0,:,:])
for i in range(1,X.shape[0]):
res += corr2d(X[i,:,:],K[i,:,:])
return res
if __name__ == '__main__':
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]]])
res = corr2d_multi_in(X, K)
print(res)
和卷积层类似,池化层每次输入数据的一个固定形状的范围中的元素进行计算。而与卷积核不同的是,池化核内部没有参数,一般是对范围内部的数据取平均值或者最大值。这种运算也称为最大池化或者平均池化。池化核的运动方式和卷积和的运动方式类似,从左上方开始,从左往右,从上往下的顺序,依次在输入矩阵上进行滑动。我们以一个取最大值的例子来展示一下:
与卷积相似的是,pooling层也可以进行padding和stride的操作。过程类似,这里就不在赘述了。
我们来简单的实现一下pooling的操作:
#encoding=utf-8
import torch
import torch.nn as nn
def pool2d(X,pool_size,mode='max'):
X = X.float()
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
if __name__ == '__main__':
X = torch.tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
res = pool2d(X, (2, 2))
print(res)
res2 = pool2d(X,(2,2),'avg')
print(res2)
在本节的最后,我们来看一下在nn模块中的池化操作,在nn中的池化包括:
在这一节中主要是对一般的卷积过程的总结和实现。主要描述了使用Pytorch来实现一般的卷积过程,同时叙述了在nn模块中的卷积核池化层的定义和参数描述。