学习目标:学习CNN基础和原理;使用Pytorch框架构建CNN模型,并完成训练
卷积神经网络(简称CNN)是是一类特殊的人工神经网络,是深度学习中重要的一个分支。它专门用来处理具有类似网格结构的数据,比如说时间序列数据(可以认为是在时间轴上有规律地采样形成的一维网格)以及图像数据(二维像素网格)。
CNN在很多领域都表现优异,精度和速度比传统计算学习算法高很多。特别是在计算机视觉领域,CNN是解决图像分类、人脸识别、图像检索、物体检测和语义分割的主流模型。近年来卷积神经网络也广泛地应用到自然语言处理、推荐系统等领域。
卷积神经网络最早是用来处理图像信息,在用全连接前馈网络来处理图像时会存在以下两个问题:
参数太多
如果输入的图像大小为100 x 100,那么在全连接情况下,输入层到第一层隐藏层需要训练的参数有(100 x 100) x (100 x 100) = 100000000个(假设隐藏层的神经个数和输入层个数相同),很明显参数的规模过于庞大,可能会导致整个神经网络的训练效率很低,而且很容易出现过拟合。
局部不变性特征
自然图像中的物体都具有局部不变性特征,比如说尺寸缩放、平移、旋转等操作不影响其语义信息。而全连接前馈网络很难提取这些局部不变性特征,一般需要进行数据增强来提高性能。
第一法宝:局部感知
针对上面参数过多的问题,我们可以进行以下优化,将隐藏层中的每个神经元只与输入层的部分神经元相连接。同样,假设输入的图像大小为100 x 100,全连接情况下,输入层到第一层隐藏层需要训练的参数有(100 x 100) x (100 x 100) = 100000000个;而在局部感知的情况下,我们假设隐藏层中的每个神经元只与输入层的10 x 10个输入(即10 x 10个像素点)相连,则需要训练的参数为(100 x 100) x (10 x 10) = 1000000个,比原来降低了100倍。
第二法宝:参数共享(以及卷积过程)
其实上面的参数个数还是很多,我们需要进行进一步优化。我们可以假设输入层中每10 x 10个输入连接到一个隐藏层中的参数是一样的。那么我们需要训练的参数数量瞬间降低到了100,是一个可以轻易进行训练的数量级。
那么你可能会问,这样的假设合理么?
答案是:从理论和思想上来说,是合理的,从实践结果的证明来看,是非常好的。这个假设意味着什么呢?这100个参数(就是卷积操作)是一种特征提取,该方式与位置无关。这其中隐含的原理则是:图像的一部分的统计特性与其他部分是一样的。这也意味着我们在这一部分学习的特征也能用在另一部分上,所以对于这个图像上的所有位置,我们都能使用同样的学习特征。再说白一点儿,一张图片,左半边和右半边的特点,风格一般情况下是一样的,哪怕毕加索这种抽象派大师,喜欢画左右脸不一样的抽象人物,其左右脸的风格也是相同的,找一个毕加索的画作,让你看,你可能一眼就看出来作者十有八九是毕加索,遮住图片的左侧,你可能也比较确认是毕加索的画作,遮住右边也是一样的,这个说法不太严谨,这个“风格”其实就是图像中的各种统计特征(图像编程了数值,那么任何位置都会有一些统计指标,比如极值,均值等)。
这个10 x 10的参数矩阵,就是所谓的卷积核!!!而卷积过程就如下图所示(卷积核为3x3)。
第三法宝:多核卷积
上面所述只有一个10 x 10的卷积核,有100个参数,显然,特征提取是不充分的,我们可以添加多个卷积核,比如32个卷积核,可以学习32种特征。这样,通过多个卷积核的操作,对图像的特征提取就更加充分了。
第四法宝:池化
有时图像太大,即使我们参数不太多,但图像的像素实在太多,导致卷积操作后,我们得到的结果仍然过大。我们需要减少训练参数的数量,它被要求在随后的卷积层之间周期性地引进池化层。池化的一个目的是减少图像的空间大小。池化在每一个纵深维度上独自完成,因此图像的纵深保持不变。池化的主要形式有最大池化和平均池化。
池化还有一个目的是保持平移不变性。卷积对输入有平移不变性,池化对特征有平移不变性。平移不变性是什么呢?因为卷积核是在输入图或者feature maps上滑动,或者说平移,每次平移时,因此假设使用max pooling,会过滤掉那些不明显、未被激活的特征。
下图是最大池化的一个例子。在这里,我们把步幅定为 2,池化尺寸也为 2。也就是对下图左侧4 x 4的矩阵,用一个2 x 2的窗口去以2为步长去遍历,再直观的说,我们按照横向和纵向两条中轴线将他切成4个2 x 2的矩阵,然后取每个矩阵的最大值,作为池化后的结果,就得到了下图右侧的池化结果。最大化执行也应用在每个卷机输出的深度尺寸中。正如你所看到的,最大池化操作后,4 x 4 卷积的输出变成了 2 x 2。
AlexNet 一些性质:
VGG-16 一些性质:
在Pytorch中构建CNN模型非常简单,只需要定义好模型的参数和正向传播即可,Pytorch会根据正向传播自动计算反向传播。
下面的代码中构建了一个非常简单的CNN模型,并进行了训练。这个CNN模型包括两个卷积层,最后并联6个全连接层进行分类。
import torch
torch.manual_seed(0)
torch.backends.cudnn.deterministic = False
torch.backends.cudnn.benchmark = True
import torchvision.models as models
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data.dataset import Dataset
# 定义模型
class SVHN_Model1(nn.Module):
def __init__(self):
super(SVHN_Model1, self).__init__()
# CNN提取特征模块
self.cnn = nn.Sequential(
nn.Conv2d(3, 16, kernel_size=(3, 3), stride=(2, 2)),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(16, 32, kernel_size=(3, 3), stride=(2, 2)),
nn.ReLU(),
nn.MaxPool2d(2),
)
#
self.fc1 = nn.Linear(32*3*7, 11)
self.fc2 = nn.Linear(32*3*7, 11)
self.fc3 = nn.Linear(32*3*7, 11)
self.fc4 = nn.Linear(32*3*7, 11)
self.fc5 = nn.Linear(32*3*7, 11)
self.fc6 = nn.Linear(32*3*7, 11)
def forward(self, img):
feat = self.cnn(img)
feat = feat.view(feat.shape[0], -1)
c1 = self.fc1(feat)
c2 = self.fc2(feat)
c3 = self.fc3(feat)
c4 = self.fc4(feat)
c5 = self.fc5(feat)
c6 = self.fc6(feat)
return c1, c2, c3, c4, c5, c6
model = SVHN_Model1()
下面是训练代码:
# 损失函数
criterion = nn.CrossEntropyLoss()
# 优化器
optimizer = torch.optim.Adam(model.parameters(), 0.005)
loss_plot, c0_plot = [], []
# 迭代10个Epoch
for epoch in range(10):
for data in train_loader:
c0, c1, c2, c3, c4, c5 = model(data[0])
loss = criterion(c0, data[1][:, 0]) + \
criterion(c1, data[1][:, 1]) + \
criterion(c2, data[1][:, 2]) + \
criterion(c3, data[1][:, 3]) + \
criterion(c4, data[1][:, 4]) + \
criterion(c5, data[1][:, 5])
loss /= 6
optimizer.zero_grad()
loss.backward()
optimizer.step()
loss_plot.append(loss.item())
c0_plot.append((c0.argmax(1) == data[1][:, 0]).sum().item()*1.0 / c0.shape[0])
print(epoch)
当然为了追求精度,也可以使用在ImageNet数据集上的预训练模型,具体方法如下:
class SVHN_Model2(nn.Module):
def __init__(self):
super(SVHN_Model1, self).__init__()
model_conv = models.resnet18(pretrained=True)
model_conv.avgpool = nn.AdaptiveAvgPool2d(1)
model_conv = nn.Sequential(*list(model_conv.children())[:-1])
self.cnn = model_conv
self.fc1 = nn.Linear(512, 11)
self.fc2 = nn.Linear(512, 11)
self.fc3 = nn.Linear(512, 11)
self.fc4 = nn.Linear(512, 11)
self.fc5 = nn.Linear(512, 11)
def forward(self, img):
feat = self.cnn(img)
# print(feat.shape)
feat = feat.view(feat.shape[0], -1)
c1 = self.fc1(feat)
c2 = self.fc2(feat)
c3 = self.fc3(feat)
c4 = self.fc4(feat)
c5 = self.fc5(feat)
return c1, c2, c3, c4, c5
通过该任务的学习,基本了解了CNN相关的基础知识和原理,以及几个比较经典的CNN网络。并学会了使用PyTorch框架构建CNN模型和训练,然后PyTorch里面还有很多预训练模型,我们可以通过迁移学习来直接使用这些预训练模型。
虽然理论知识的学习很重要,但是我们也要重视实践。怎么样选择最合适的模型,怎么样进行训练,怎么样解决过拟合,以及怎么样进一步优化模型,提高准确率,这都是我们在实际应用中将会遇到的问题。只有真正经历了这些问题,并且逐一解决,我们才能得到成长。