在用Pytorch构建神经网络时,全连接网络的构建很简单:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(100, 512)
self.fc2 = nn.Linear(512, 10)
def forward(self, x):
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
return F.log_softmax(x, dim=1)
只需要上一层的输出神经元数等于下一层的输入神经元数即可。但一旦到了卷积神经网络,卷积和池化操作会带来数据尺寸的变化,对于初学者(比如我)来说,往往不是很友好,下面是识别MNIST数据集的CNN:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 20, 5, 1)
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(20, 50, 5, 1)
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(4 * 4 * 50, 500)
self.fc2 = nn.Linear(500, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool1(x)
x = F.relu(self.conv2(x))
x = self.pool2(x)
x = x.view(-1, 4 * 4 * 50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x, dim=1)
其中的参数对于初学者来说是confusing的,我们这里想把这些参数的含义和设置方法摸清,为后面的工作带来方便。
前置文章:Understanding of a Convolutional Neural Network - 邢存远的博客
在torch.nn
中有三种卷积算子:Conv1d
、Conv2d
和Conv3d
,分别用来处理一维、二维和三维数据,二维对应图片,三维对应视频;一维卷积的话,笔者在Kaggle中看到过有人用一维卷积来进行数据降维。
直接看参数:
in_channels
:输入通道数;out_channels
:输出通道数;kernel_size
:卷积核大小;stride=1
:滑动步长;padding=0
:边缘填充的单元数。不常用的参数这里忽略不谈。我们来看一个合法的一维卷积运算:
import torch
m = torch.nn.Conv1d(
in_channels=16,
out_channels=33,
kernel_size=3,
stride=2,
)
i = torch.randn(20, 16, 50)
o = m(i)
print(o.shape) # (20, 33, 24)
这里的i
是一个20个样本(batch_size)的数据,对应i
的第一维是20;输入通道数为16(输入通道数类比数据的层数,RGB图片的输入通道数就是3),对应i
的第二维是16;每个通道就是一个长度为50的向量,对一个i
的第三维是50。
当我们用一个卷积核对数据i
中的一个样本,比如i[0]
进行卷积,考虑多个通道,卷积生成的是一个长度为
的向量。而我们又规定了输出通道数为33,对应使用33个卷积核进行卷积,因此输出是(20, 33, 24)的张量。
网上资料(包括论文)常常将卷积核定义为一个普通矩阵,比如锐化卷积核就是
\[\begin{bmatrix} 0&-1&0\\ -1&5&-1\\ 0&-1&0 \end{bmatrix}\]对图像进行处理后,图像中的边缘会加粗(详情参考卷积与池化的探究 - 邢存远的博客 Welt Xing’s Blog (welts.xyz))。但这样其实忽略了RGB图片是有3个通道的,我们这里的锐化操作,其实是对每个通道(对应一个大矩阵)用上面的矩阵进行卷积操作,然后将各个通道卷积的结果合并成一个新的RGB图片(关于RGB图片相关可参考Pillow - 邢存远的博客 Welt Xing’s Blog (welts.xyz))。从这种意义上说,真正的锐化卷积核是一个三维张量(高阶张量参考高阶张量与Pytorch - 邢存远的博客 Welt Xing’s Blog (welts.xyz)):
\[\begin{bmatrix} \begin{bmatrix} 0&-1&0\\ -1&5&-1\\ 0&-1&0 \end{bmatrix}&\begin{bmatrix} 0&-1&0\\ -1&5&-1\\ 0&-1&0 \end{bmatrix}& \begin{bmatrix} 0&-1&0\\ -1&5&-1\\ 0&-1&0 \end{bmatrix} \end{bmatrix}\]形象化地说就是一个3*3*3的立方体,立方体的每一层都是相同的矩阵。
在这里,我们的样例是16个通道,卷积核长度为3,考虑通道数,这里的卷积核是16*3的矩阵,16个通道各自卷积出来的结果被合并成一个通道,也就是输出通道,而当我们采用了$n$个不同的卷积核,那么就会有$n$个输出通道,参数
out_channels
就对应的这个$n$.
Conv2d,也就是二维卷积,参数和上面是一样的,但由于是2个维度的卷积,不同维度的kernel_size
、stride
和padding
都可以视需要而不同,比如下面两种卷积层都是可行的:
# With square kernels and equal stride
m = nn.Conv2d(16, 33, 3, stride=2)
# non-square kernels and unequal stride and with different padding
m = nn.Conv2d(16, 33, (3, 5), stride=(2, 1), padding=(4, 2))
类比前面的一维卷积中,每个通道中是一个向量,那么二维卷积下每个通道就是一个矩阵,可以类比三通道的RGB图片。一个合法的二维卷积运算:
import torch
m = torch.nn.Conv2d(16, 33, 3, stride=2)
i = torch.randn(20, 16, 50, 100)
o = m(i)
print(o.shape) # (20, 33, 24, 49)
对于o
的尺寸,20和33很好解释,24和49分别由卷积尺寸的计算公式获得:
三维卷积主要是应用于视频数据,多出来一个表征时间的帧维度,三维卷积层的定义类比上面,例如:
m = nn.Conv3d(16, 33, (3, 5, 2), stride=(2, 1, 1), padding=(4, 2, 0))
一个合法是三维卷积运算:
import torch
m = torch.nn.Conv3d(16, 33, 3, stride=2)
i = torch.randn(20, 16, 10, 50, 100)
o = m(i)
print(o.shape) # (20, 33, 4, 24, 49)
这里的输入数据,每个通道中是一个10帧的画面,每一帧都是一个50*100的二维数据(图像)。
池化往往出现在卷积层的后面,用于对数据进行降维,分为平均池化和最大池化;池化同样可以分为一维、二维和三维池化,对应的,Pytorch中有6中不同的池化层函数:MaxPool1d, AvgPool1d, MaxPool2d, AvgPool2d, MaxPool3d和AvgPool3d. 为简单起见,我们在这里只讨论最大池化的函数。
池化的参数比卷积要少,因为池化操作是在每个通道独立进行的,所以通道数是不变的:
kernel_size
:单次池化操作的范围大小;stride=1
:滑动步长;padding=0
:填充数.一个合法的卷积操作:
import torch
m = torch.nn.MaxPool1d(3, stride=2)
i = torch.randn(20, 16, 50)
o = m(i)
print(o.shape) # (20, 16, 24)
这里通道数不变,只是每个通道的向量长度发生了变化,变化遵从公式
\[O=1+\bigg\lfloor\dfrac{N-F+2P}{S}\bigg\rfloor\]至于二维池化和三维池化,我们可以类比上面的一维池化和前面的卷积方法,在此不过多赘述。
我们回顾前面提到的手写数字识别的卷积神经网络:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 20, 5, 1)
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(20, 50, 5, 1)
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(4 * 4 * 50, 500)
self.fc2 = nn.Linear(500, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = self.pool1(x)
x = F.relu(self.conv2(x))
x = self.pool2(x)
x = x.view(-1, 4 * 4 * 50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x, dim=1)
逐层分析,同时计算输入输出各层数据的尺寸: