10.18 卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

互联网发展至今,已经存储了海量的网络图片,但是这些图片被形象地称为互联网的“暗物质”,因为现在的计算机还难以分类或识别这些非结构性的图片数据。在早期的图像识别研究中,使用人工提取的特征造成识别效果不佳。卷积神经网络(Convolutional Neural Network, CNN)的出现给图像识别领域带来了崭新的风气,如今,CNN图像识别技术的正确率已经可以达到人类水平。卷积神经网络的兴起大大促进了深度学习研究的发展。

下面我们来研究下卷积神经网络的基本原理,为接下来的实战打好基础。

仿生模型

20 世纪60 年代,神经科学家们研究了猫的脑皮层中用于局部敏感和方向选择的神经元,在这个过程中,他们发现猫的脑皮层所具有的独特网络结构可以有效地降低反馈神经网络的复杂性,研究结果显示:视觉系统的信息处理是分级的。

大脑分层处理的视觉原理如图1所示:首先,光信号进入瞳孔(视网膜),接着大脑皮层的初级视觉细胞(即V1 区)对信号进行初步处理,发现图像的边缘和方向;然后进入下一层视觉细胞(即V2 区)进行抽象,发现物体的形状;最后在V4 区进一步抽象出物体的概念。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图1 人脑视觉处理机制

卷积神经网络模型的物体识别模仿了人脑的视觉处理机制,采用分级提取特征的原理,每一级的特征均由网络学习提取,识别效果优于人工选取特征的算法。例如在人脸识别过程中,最底层特征基本上是各方向上的边缘,越往上的神经层越能提取出人脸的局部特征(比如眼睛、嘴巴、鼻子等),最上层由不同的高级特征组合成人脸的图像。该模型最早在1998 年由Yann LeCun 提出并应用在手写字体识别上(MINST),LeCun 提出的网络称为LeNet,其网络结构如图2所示,输入的手写字体图片经过两次卷积和池化,进入全连接层后分类输出10 种结果。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图2 LeNet 结构示意图

LeNet 结构简单却完美地诠释了卷积神经网络的结构和其惊人的识别成效,被称为深度神经网络的“果蝇”。囿于当时计算硬件的落后和网络图片数量的不足,卷积神经网络无法在分辨率较大的图片上展示它的巨大潜力,当时并没有引起广泛关注。在2012 年的ImageNet 计算机视觉识别比赛中,卷积神经网络AlexNet 以准确率领先第二名10% 的显著差距获得了第一名。从此,大批计算机视觉科学家纷纷加入了卷积神经网络的研究当中,深度学习的热潮开始兴起。

卷积

在我们前面所介绍的神经网络 中,输入层被描述为一列神经元,而在卷积神经网络里,我们把输入层看作二维的神经元。如果输入是像素大小为28×28 的图片①,则可以看作28×28 的二维神经元,它的每一个节点对应图片在这个像素点的灰度值,如图3所示。

① PX(Picture Element)就是我们常说的像素,它是构成影像的最小单位。像素是一个相对的单位,当图片尺寸以像素为单位时,我们需要指定其固定的分辨率,才能将图片尺寸与现实中的实际尺寸相转换,因此经常会省略PX和像素,如本句表达为“像素大小为28×28 的图片”或“大小为28×28 的图片”。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图3 输入图片的二维展示

在传统的神经网络中,我们会把输入层的节点与隐含层的所有节点相连。卷积神经网络中,采用“局部感知”的方法,即不再把输入层的每个节点都连接到隐含层的每一个神经元节点上。如图4所示,我们把相邻的5×5 局部区域内的输入节点连接到了第一个隐含层的一个节点上。这个相邻的区域,我们称为局部感知域,可以把它看成是一个小窗口,我们也称之为过滤器。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图4 输入层与隐含层的连接

如图5 所示,我们可以在整个输入图像上滑动这个过滤器。对于每一个过滤器,都有一个隐含层的神经元与之对应。将过滤器向右滑动一个单位后对应一个隐含层的神经元节点。以此类推,我们构建出了第一个隐含层。在这个例子中,输入是28×28,并且使用5×5 的过滤器,那么第一个隐含层的大小为24×24。因为过滤器的小窗口只能向右和向下移动23 像素,再往下或者往右移动就会移出图像的边界。在这个图中,过滤器使用的滑动单位为1 像素,实际上,我们也可以让滑动单位不止1 像素,这个滑动值的英文叫stride,我们也称它为步长。在卷积神经网络中,这种隐含层被称为“特征图”。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图5 过滤器的移动

在进行卷积操作之前,需要定义一个过滤器,其中的每一格都有一个权重值。卷积的过程是将每个格子中的权重值与图片对应的像素值相乘并累加,所得到的值就是特征图中的一个像素值。如图6 所示,我们对过滤器和卷积进行一个比较具体而详细的说明。假设我们输入的是5×5 的图片,过滤器可以看作一个小窗口,这个小窗口大小是3×3,里面包含了9 个权重值,我们将9 个权重值分别与输入的一部分像素值相乘后进行累加,这个过程被称为“卷积”。图中小窗口覆盖的输入区域卷积结果是隐含层的灰色部分,结果为2。隐含层的结果就是我们通过卷积生成的特征图。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图6 过滤器的卷积过程

此外,我们发现,即便步长为1,经过卷积之后的特征图尺寸也会缩小。由于过滤器在移动到边缘的时候就结束了,中间的像素点比边缘的像素点参与计算的次数要多。因此越是边缘的点,对输出的影响就越小,我们就有可能丢失边缘信息。为了解决这个问题,我们进行填充,英文叫padding,即在图片外围补充一些像素点,并将这些像素点的值初始化为0。

为什么卷积

在传统全连接的神经网络中,如果要对一张图片进行分类,连接方式如图7所示。我们把一张大小为100×100 的图片的每个像素点都连接到每一个隐含层的节点上,如果隐含层的节点数为10 000,那么连接的权重总数则为10的8次方个。当图片像素更大,隐含层的节点数目更多时,则需要更加庞大的权重数目。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图7 全连接神经网络的权重数目

在卷积神经网络中,我们不再需要如此庞大的权重数目。如图8 所示,在利用10×10 的过滤器对100×100 的原图进行卷积时,该过滤器在不断滑动的过程中对应生成一张特征图,即一个过滤器(100 个权重值)可对应一张特征图。如果我们有100 张特征图,则一共只需要10的4次方个权重值。

如此一来,在一个隐含层的情况下,卷积神经网络的权重数目可以减小至全连接神经网络权重数目的一万分之一,大大减少计算量,提高计算效率。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图8 卷积神经网络的权重数目

在实际训练的过程中,第一层的每一个过滤器的权重值会不断地被更新优化,最终形成如图3-40所示的结果,每个过滤器的可视化纹理模式基本上反映了各个方向上的边缘特征。图9 为我们展示了24 个过滤器的纹理模式,这24 种边缘可以描绘出我们的原图。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图9 过滤器的可视化

池化

池化的目的是降低数据的维度,过程很简单,实际上就是下采样。具体过程如图10 所示,假如特征图的尺寸是8×8,池化的窗口为4×4,则对特征图按照每4×4 进行一次采样,生成一个池化特征值。这样一来,8×8 的特征图可以生成一个2×2 的池化特征图。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图10 池化过程

在实际应用中,生成池化特征的方式一般有两种:最大值池化(Max-Pooling)与平均值池化(Mean-Pooling)。其中,最大值池化的方法是将特征图中池化窗口范围内的最大值作为池化结果的特征值,过程如图11所示;平均值池化的方法是将特征图中池化窗口范围内的所有值进行平均后作为池化的特征值。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图11 最大池化

实战:用PyTorch 构建卷积神经网络

下面,我们尝试利用PyTorch 构建卷积神经网络。这里以LeNet 网络为例,利用MINST 手写字体库进行训练,实现一个手写体的自动识别器。

1. LeNet——深度学习界的“果蝇”

LeNet是1998年由Yann LeCun提出的一种卷积神经网络,当时已经被美国大多数银行用于识别支票上的手写数字。详细资料可以浏览LeNet-5官网http://yann.lecun.com/exdb/lenet/,图12是LeNet进行手写体识别时,输入图片、各层特征图的可视化形式及其最终的识别结果。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图12 LeNet 进行手写体识别

LeNet-5 的结构如图2所示:

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

其网络结构比较简单,如果不包括输入,它一共有7 层,输入图像的大小为28×28。通过上面的学习,我们可以分辨出C1 层为6 张特征图,由6 个大小为5×5 的卷积核过滤生成,特征图的尺寸为24×24。S2 是池化层,采用最大池化法,池化窗口大小为2×2。因此,6 张24×24 的特征图池化后会生成6 张12×12 的池化特征图。C3 层是卷积层,一共有16 个过滤器,生成16 张8×8 大小的特征图,卷积核大小为5×5。这里,S2 与C3 的连接组合方式并不是固定的,C3层的每一张特征图可以连接S2 层中的全部或者部分特征图。一般情况下,为了更好地降低总连接数,并不使用全部特征图的全连接方式。S4 是池化层,它是由16 张8×8 的特征图最大池化生成的16 张4×4的特征图,其池化核大小为2×2。F5 是全连接层,一共有120 个神经元节点,每个节点与S4 层的16张池化特征图进行连接。因此,F5 层与S4 层是全连接。F6 层有84 个神经元节点,与F5 进行全连接。最后一层为输出层,把输出的神经元节点数设为10,代表0 到9 的分值。

2. 准备数据集

MNIST 是一个手写数字数据库(官网地址:http://yann.lecun.com/exdb/mnist/)。如图13 所示,该数据库有60 000 张训练样本和10 000 张测试样本,每张图的像素尺寸为28×28。其中train-imagesidx3-ubyte.gz 为训练样本集,train-labels-idx1-ubyte.gz 为训练样本的标签集,t10k-images-idx3-ubyte.gz为测试样本集,t10k-labels-idx1-ubyte.gz 为测试样本的标签集。这些图片文件均被保存为二进制格式。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图13 MNIST 官网

我们现在需要用到上述这些数据集。幸运的是,PyTorch 为我们编写了快速下载并加载MNIST 数据集的方法。为了方便图像的运用开发,PyTorch 团队为我们专门编写了处理图像的工具包:torchvision。

torchvision里面包含了图像的预处理、加载等方法,还包括了数种经过预训练的经典卷积神经网络模型。

下面用一个例子给大家介绍。首先我们从torchvision库中导入datasets和transforms,datasets是加载图像数据的方法,transforms是图像数据预处理的方法:

from torchvision import datasets, transforms

然后我们使用transforms.Compose()函数设置预处理的方式:

transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])

这里可依次填写需要进行数据预处理的方法。这个例子中,我们只用了两种方法,其中transforms.ToTensor()是将数据转化为Tensor对象,transforms.Normalize()是将数据进行归一化处理。

接下来我们使用datasets.MNIST()函数分别下载训练数据集和测试数据集:

trainset = datasets.MNIST('data', train=True, download=True, transform=transform)
testset = datasets.MNIST('data', train=False, download=True, transform=transform)

data.MNIST的第一个参数指定了数据集下载并存储的目标文件夹;train=True表示加载训练数据集,train=False表示加载测试数据集;这里我们令download=True,代表使用这个函数帮助我们自动下载MNIST数据集;transform的设置代表我们使用刚才定义的数据预处理方法。

接下来,运行上述的代码,程序就会自动下载MNIST数据集:

Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz
Processing...

Done!

显示以上信息后,如图14所示,我们会发现代码所在目录下新增了一个data文件夹,里面包含了processed和raw文件夹,其中raw文件夹里面包含了MNIST数据集的训练和测试的数据与标签。

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

图14 自动下载MNIST数据集

3. 构建LeNet

准备好数据之后,我们可以开始构建LeNet模型。我们知道,初始化函数需要先运行父类的初始化函数。先定义C1卷积层,PyTorch中的nn.Conv2d()函数为我们简化了卷积层的构建。在C1层的定义中,第一个参数为1,代表输入1张灰度图;第二个参数为6,代表输出6张特征图;第三个参数是一个元组 (5, 5),也可以简化成5,代表大小为5×5的卷积核过滤器。然后定义C3卷积层,C3输入6张特征图,输出16张特征图,过滤器大小仍为5×5。接着定义全连接层,其中fc1是由池化层S4中的所有特征点(共16×4×4个)全连接到120个点,fc2是由120个点全连接到84个点,fc3是由84个点全连接到输出层中的10个输出节点。

定义完初始函数之后,我们开始定义foward()函数。c1卷积之后,使用relu()函数增加了网络的非线性拟合能力,接着使用F.max_pool2d()函数对c1的特征图进行池化,池化核大小为2×2,也可以简化参数为2。经过两轮卷积和池化之后,使用view函数将x的形状转化成1维的向量,其中我们自定义了num_flat_features()函数来计算x的特征点的总数。在自定义的num_flat_features()函数中,由于PyTorch只接受批数据输入的方式(即同时输入好几张图片进行处理),所以我们在经过view()函数之前的x是4个维度的。假设我们批量输入4张图片,则x.size()的结果为(4,16,4,4)。我们使用x.size()[1:]返回x的第二维以后的形状,即(16,4,4)。因此,按照num_flat_features()函数返回的数值为16×4×4,即256,随后进行全连接fc1、fc2和fc3:

class LeNet(nn.Module):
def __init__(self):
super(LeNet,self).__init__()
self.c1 = nn.Conv2d(1,6,(5,5))
self.c3 = nn.Conv2d(6,16,5)
self.fc1 = nn.Linear(16*4*4,120)
self.fc2 = nn.Linear(120,84)
self.fc3 = nn.Linear(84,10)
def forward(self,x):
x = F.max_pool2d(F.relu(self.c1(x)),2)
x = F.max_pool2d(F.relu(self.c3(x)),2)
x = x.view(-1,self.num_flat_features(x))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

def num_flat_features(self,x):
size = x.size()[1:]
num_features = 1
for s in size:
num_features *= s
return num_features

接下来我们初始化LeNet,并定义损失函数为交叉熵函数,优化器为随机梯度下降:

CUDA = torch.cuda.is_available()
if CUDA:
lenet = LeNet().cuda()
else:
lenet = LeNet()
criterion=nn.CrossEntropyLoss()
optimizer = optim.SGD(lenet.parameters(),lr=0.001,momentum=0.9)

随后,用PyTorch的数据加载工具DataLoader来加载训练数据:

trainloader = torch.utils.data.DataLoader(trainset,batch_size=4, shuffle=True, 
num_workers=2)

上面DataLoader()的参数中,batch_size表示一次性加载的数据量,shuffle=True表示遍历不同批次的数据时打乱顺序,num_workers=2表示使用两个子进程加载数据。

4. 训练

现在开始训练LeNet,我们将完全遍历训练数据2次。为了方便观察训练过程中损失值loss的变化情况,定义变量running_loss。一开始,我们将running_loss设为0.0,随后对输入每一个训练数据后的loss值进行累加,每训练1000次打印一次loss均值,并清零。enumerate(trainloader,0)表示从第0项开始对trainloader中的数据进行枚举,返回的i是序号,data是我们需要的数据,其中包含了训练数据和标签。随后我们进行前向传播和反向传播。代码如下:

def train(model,criterion,optimizer,epochs=1):
for epoch in range(epochs):
running_loss = 0.0
for i, data in enumerate(trainloader,0):
inputs,labels = data
if CUDA:
inputs,labels = inputs.cuda(),labels.cuda()
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs,labels)
loss.backward()
optimizer.step()

running_loss += loss.item()
if i%1000==999:
print('[Epoch:%d, Batch:%5d] Loss: %.3f' % (epoch+1, i+1,
running_loss / 1000))
running_loss = 0.0

print('Finished Training')

train(lenet,criterion,optimizer,epochs=2)

运行结果如下:

[Epoch:1, Batch: 1000] loss: 1.336
[Epoch:1, Batch: 2000] loss: 0.293
[Epoch:1, Batch: 3000] loss: 0.213
[Epoch:1, Batch: 4000] loss: 0.157
[Epoch:1, Batch: 5000] loss: 0.144
[Epoch:1, Batch: 6000] loss: 0.120
[Epoch:1, Batch: 7000] loss: 0.109
[Epoch:1, Batch: 8000] loss: 0.106
[Epoch:1, Batch: 9000] loss: 0.117
[Epoch:1, Batch:10000] loss: 0.098
[Epoch:1, Batch:11000] loss: 0.072
[Epoch:1, Batch:12000] loss: 0.095
[Epoch:1, Batch:13000] loss: 0.084
[Epoch:1, Batch:14000] loss: 0.073
[Epoch:1, Batch:15000] loss: 0.079
[Epoch:2, Batch: 1000] loss: 0.065
[Epoch:2, Batch: 2000] loss: 0.065
[Epoch:2, Batch: 3000] loss: 0.072
[Epoch:2, Batch: 4000] loss: 0.070
[Epoch:2, Batch: 5000] loss: 0.055
[Epoch:2, Batch: 6000] loss: 0.047
[Epoch:2, Batch: 7000] loss: 0.058
[Epoch:2, Batch: 8000] loss: 0.051

[Epoch:2, Batch: 9000] loss: 0.066
[Epoch:2, Batch:10000] loss: 0.047
[Epoch:2, Batch:11000] loss: 0.066
[Epoch:2, Batch:12000] loss: 0.059
[Epoch:2, Batch:13000] loss: 0.054
[Epoch:2, Batch:14000] loss: 0.058
[Epoch:2, Batch:15000] loss: 0.062
Finished Training

从结果中可以发现,我们的训练是有效的,损失值loss的平均值从1.336逐渐优化并下降为0.062。

5. 存储与加载

我们训练完神经网络之后,需要存储训练好的参数,以方便以后使用。存储的方式有两种。

(1) 存储和加载模型

存储:

torch.save(lenet, 'model.pkl')

加载:

lenet = torch.load('model.pkl')

利用torch.save()函数直接传入整个网络模型lenet,并设置存储的路径。这种方法虽然简洁,但在神经网络比较复杂的时候,会占用较大的存储空间。

(2) 存储和加载模型参数

存储:

torch.save(lenet.state_dict(),'model.pkl')

加载:

lenet.load_state_dict(torch.load('model.pkl'))

这种方法直接保存模型参数,节省了空间,但是它不存储模型的结构,所以在加载时需要先构造好模型结构。

在这个例子中,我们倾向于使用第二种方法,利用os包的exists()方法检查是否存在模型参数文件。构造load_param()和save_param()函数方便随时调用:

def load_param(model,path):
if os.path.exists(path):
model.load_state_dict(torch.load(path))
def save_param(model,path):
torch.save(model.state_dict(),path)

6. 测试

神经网络模型经过长时间的训练之后,能在训练集的数据上表现得很好,并不一定代表它在训练集以外的数据上同样表现优异。为了更加客观地衡量神经网络模型的识别率,我们通常需要另外一批数据进行测试。为此,设置train=False准备测试集,并设置testloader对测试集进行加载:

testset = datasets.MNIST('data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2)

下面我们对训练完成的神经网络添加测试模块:

def test(testloader,model):
correct = 0
total = 0
for data in testloader:
images, labels = data
if CUDA:
images = images.cuda()
labels = labels.cuda()
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum()
print('Accuracy on the test set: %d %%' % (100 * correct / total))

我们创建一个test()函数,传入testloader和model对象,让testloader中的测试数据经过神经网络,得到outputs,再使用torch.max(outputs.data,1)找出每一组10个输出值中最大的那个值,并将该值所在的序号保存在predicted变量中。total用于累计labels的总数,correct用于累计正确的结果总数:

load_param(lenet,'model.pkl')
train(lenet,criterion,optimizer,epochs=2)
save_param(lenet,'model.pkl')
test(testloader,lenet)

可以在训练前加载之前训练完成的参数,训练后对参数进行保存,接着进行测试,测试结果如下:

Accuracy on the test set: 98 %

fastai库

PyTorch的生态圈非常丰富,AllenNLP、ELP、fastai、Glow、GPyTorch、Horovod、ParlAI、Pyro、TensorLy等库都是基于PyTorch开发的AI工具,可以帮助我们更加快速地开发AI产品。fastai库(https://www.fast.ai)致力于使用当前最先进的实践方法去简化传统的神经网络训练方法,提高训练速度。fastai可以帮助程序员更加高效地训练神经网络,用它来实现MNIST手写字体识别只需要短短7行代码。如果系统安装的PyTorch版本在1.0以上,那么可以直接安装并使用fastai 1.0版本。

1. 安装

使用conda命令进行安装:

conda install -c fastai fastai

我们也可以使用pip命令进行安装:

pip install fastai

2. 代码实践

导入fastai中的vision包:

from fastai.vision import *

使用untar_data()函数,传入参数URLs.MNIST_SAMPLE,下载并解压MNIST数据包:

path = untar_data(URLs.MNIST_SAMPLE)

创建一个DataBunch:

data = ImageDataBunch.from_folder(path)

初始化一个Learner,传入数据data,设置模型为resnet18。调用fit()函数开始训练,fit()函数的参数为训练次数,这里仅训练1次:

learner = create_cnn(data, models.resnet18, metrics=accuracy)
learner.fit(1)

训练和测试完成后,打印如下:

epoch train loss valid loss accuracy 

0 0.078617 0.041789 0.984789

除了图片识别之外,fastai还提供了自然语言处理、协同过滤以及结构数据处理等应用,由于篇幅有限,本文不再细致讲解。

——

本文我们介绍了卷积神经网络的基本原理,用当下很流行的深度学习框架PyTorch构建了一个卷积神经网络。最后,我们还了解了fastai,可以用短短几行代码构建深度卷积网络,完成了识别手写字体的小实验。

——本文节选自《PyTorch深度学习入门》

卷积神经网络,还是这样理解更简单!PyTorch深度学习入门

这是文章阅读量10万+的作者倾力打造的一份超简单PyTorch入门教程。对于想要理解深度学习的人的第一选择。

它更适合小白的思路与讲解方式:从硬件挑选、系统配置开始,图文并茂,手把手教你搭建神经网络。

书中从如何挑选硬件到神经网络的初步搭建,再到实现图片识别、文本翻译、强化学习、生成对抗网络等多个目前最流行的深度学习应用。书中基于目前流行的PyTorch框架,运用Python语言实现了各种深度学习的应用程序,让理论和实践紧密结合。

第一部分 基础篇

第1章 准备工作

第2章 Tensor基础

第3章 深度学习基础

第二部分 实战篇

第4章 迁移学习

第5章 序列转序列模型

第6章 生成对抗网络

第7章 深度强化学习

第8章 风格迁移

第三部分 高级篇

第9章 PyTorch扩展

第10章 PyTorch模型迁移

第11章 PyTorch可视化

第12章 PyTorch的并行计算


分享到:


相關文章: