PyTorch框架深度学习笔记04(卷积、torch.nn)
本篇笔记,将会学习神经网络的起源,深度学习的兴起,以及最著名的算法卷积神经网络的原理和应用。同时,我们还会简单讲解 PyTorch 神经网络中 nn.Module 这个核心的基类。
- 卷积神经网络(Convolutional Neural Networks,CNN)由纽约大学的 Yann Lecun 于 1998 年提出,是深度学习技术中非常重要的且具有代表性的算法,其本质是一个多层感知机。
- nn (Neural Network)
nn.Module 是 PyTorch 体系下所有神经网络模块的基类(Base class for all neural network modules.)
从感知机到深度学习的兴起
MP模型的诞生
神经元,是神经系统最基本的结构和功能单位,细胞体由细胞核、 细胞膜 、细胞质组成,具有联络和整合输入信息并传出信息的作用。1943 年,神经学家 Warren McCulloch 和逻辑学家 Walter Pitts通过创造神经网络的数学模型解开了这个谜题。他们提出了数学与算法结合的方式模拟人类的思维活动,构建出了第一个数学神经元模型 McCulloch-Pitts 模型(简称MP模型),为人工智能学奠定了重要的数学基础。
正如上文所说,神经元具有整合输入信息并传出的能力,而MP则很好的实现了这一点——接收多个信号(数据),整合计算这些信号,并通过激活函数判断是否超过阈值并进行输出。MP模型展现了算法实现神经计算的潜力。
感知机模型的发展
1957 年,美国心理学家 Frank Rosenblatt 就职于康奈尔航空实验室时,提出了可以模拟人类感知能力的机器,即为感知机(Perceptron)的神经网络,它可以被视为一种最简单形式的前馈神经网络,是一种二元线性分类器。1957 年,在 Cornell 航空实验室中,他成功在 IBM 704 机上完成了感知机的仿真。两年后,他又成功实现了能够识别一些英文字母、基于感知机的计算机——Mark I perceptron,并于 1960 年 6 月 23 日,展示与众。这是人工智能发展史上里程碑式的成就。
Rosenblatt 在设计感知机时,为此设计了一种准则函数。该函数实现了神经网络的监督式学习。
其中 M 是被当前 W 错误分类的的输入向量集合。当 W^{T}P_{i}\ge 0 时,t_{i}=-1 ;当 W^{T}P_{i}< 0 时,t_{i}=1所以误差函数 E\left(W\right) 是一组正数的和,当训练集中所有输入向量都被正确分类时,其和为零。
感知机虽然在当时被寄予厚望,但感知机最终被证明不能处理诸多的模式识别问题,并且在分线性可分的数据上,感知机最终无法收敛。
此后,在众多学者的付出下,Rosenblatt 的感知机模型被不断改进,出现了多层感知机(MLP)、ADALINE 等更近一步的模型。并且提出了隐藏层 (Hidden Layers)和许多激活函数,隐藏层使得 MLP 可以捕捉和表达数据中的复杂非线性关系,而激活函数则大大提高了模型的非线性表达能力。这些新算法极大地增强了网络的学习能力,使其能够解决诸如异或问题这样非线性可分的问题。
误差反向传播算法
1986 年 Rumelhart 等人提出了人工神经网络的反向传播算法,全程误差反向传播算法(Backpropagation,BP)是对多层人工神经网络进行梯度下降的算法,该算法的开发使得神经网络在理论上可以逼近任何函数,如今已经成为深度学习领域最核心的组件。
神经网络的学习最核心的步骤便是通过数值微分进行损失函数关于权重参数的梯度,如今 PyTorch 框架和 TensorFlow 框架最强大之处也是在于灵活的自动求导功能。而误差反向传播算法便是一个能够高效计算权重梯度的方法。
反向传播算法主要由两个阶段组成:激励传播与权重更新。
- 激励传播过程分为两个阶段,分别是正向传播和反向传播。其中正向传播指的是将训练数据送入神经网络以获得预测结果,而反向传播则是通过损失函数将预测结果与目标数据求差。
- 权重更新则将输入激励和响应误差相乘,从而获得权重的梯度,随后将这个梯度乘上一个比例并取反后加到权重上。
学习过程中,我们可以反复循环迭代该过程,直到网络对输入的响应达到满意的预定的目标范围为止。
如图所示,假设存在 y=f\left(x\right) 的计算,则正向传播过程如上灰色部分(从左到右)所示,反向传播如黑色部分(方向相反)所示。反向传播的计算顺序是将上游传回的信号 E 乘以节点处的局部导数 \frac{\partial y}{\partial x} ,然后将结果传到下一个节点。
在适用反向传播算法的网络中,它通常可以快速收敛到令人满意的极小值。
深度学习的兴起
上个世纪,即使理论层面已经成熟,但是硬件的落后无法承担起计算神经网络庞大参数量的任务,神经网络领域多次陷入黑暗时代。而在 2010 年代初,得益于深度学习架构的发展和硬件的改进以及大规模的数据集,神经网络领域迎来了复兴。其中卷积神经网络便是最具有代表性的一种算法。
卷积神经网络
卷积神经网络在深度学习领域,尤其是计算机视觉领域带来了革命性的变化。卷积神经网络是一种带有卷积结构的深度神经网络,卷积结构可以减少深层网络占用的内存量,它的三个核心部分有效的减少了网络的参数个数,缓解了模型的过拟合问题。
- 局部感受野(Receptive Field)
- 权值共享(Weight Sharing)
- 池化层(Pooling Layer)
卷积和卷积核
卷积(Convolution)是卷积神经网络中最基础的算法,下面两张动态图解释了卷积的计算原理。通过卷积运算我们可以提取出图像的特征,使得输入数据的特定特征增强,这个过程称为特征映射。
在卷积神经网络中,我们通常使用多个卷积核进行卷积,以提取图像的不同特征,同时我们还需要多个卷积层,用来提取图像的深层特征。
卷积核(Kernel),本质上就是一个小型矩阵,如上图中间的 3x3 矩阵,就是一个卷积核。卷积核通过和原始图像进行卷积运算,可以提取出原始图像的部分特征。
局部感受野
一个感觉神经元的感受野(Receptive Field)是指这个位置里适当的刺激能够引起该神经元反应的区域。感受野一词主要是指听觉系统、本体感觉系统和视觉系统中神经元的一些性质。
而在卷积神经网络中,感受野指的是神经网络中每一层输出的特征图(Feature Map)中每个像素点所能映射回原始图像的区域。感受野越大,说明这层神经网络的学习更加全面;感受野越小,则说明这层神经网络学习的更加细节。
感受野大小是可以通过输入图像大学和输出特征图大小进行计算的,而我们也可以根据感受野大小来估计每一层神经网络对数据特征提取的抽象程度。
权值共享
权值共享(Weight Sharing)是指在神经网络的多个位置或层中使用相同的权重参数。这种共享机制可以显著减少模型的参数数量,从而降低计算复杂度和存储需求,并有助于提高模型的泛化能力。这个概念最早是在 LeNet-5 模型中被提出,下图是 1998 年,LeCun 发布的 LeNet-5 网络架构。
权值共享在卷积神经网络中尤为常见,特别是在卷积层中,同一个卷积核在图像的不同区域滑动,共享相同的权重参数。这个现象其实十分显然,因为在卷积核参数不变的时候,一张图片被同一个卷积核扫过,权重当然是一样的,也就是权值共享。
但是,稍微深入理解,就会发现权值共享是卷积神经网络中当之无愧的核心设计。设想一个场景,如果我们将一个256x256的原始图像传入神经网络,如果是原始的全连接神经网络,那就会产生爆炸量的数据参数,导致训练过程极其缓慢,并且对于如此多的参数,我们还需要更多的原始数据来防止过拟合。
而卷积神经网络使用卷积核,卷积核在整张图像上移动时权值不变,重复使用。比如一个 3x3 的卷积核就只有九个参数和一个偏置,这和上述情况的参数量完全不是一个数量级的,反而使用卷积核还不会导致全连接神经网络可能发生的同一个物体出现在不同位置会被当作完全不同的特征。**也就是说,无论一个特征出现在图像的某个位置,检测它的都是同一个卷积核的权重。**这就是权值共享的重要性了,卷积核的权值共享实现了特性提取的高效化和精确化。
池化层
池化层(Pooling Layer)是卷积神经网络(CNN)中的另一核心组件,通常紧跟在卷积层之后。它的核心作用是对特征图进行采样(或者说对特征进行汇聚),可以减少模型的参数量和有效降低模型的过拟合风险。
常用的池化层有两种,一种是最大池化(Max Pooling),一种是平均池化(Mean Pooling),相对来说最大池化层应用更多,很多研究已经证明了最大池化层的汇聚效果优于平均池化层。
虽然池化层有上述优点,但是也有部分观点认为可以在卷积层为卷积核设置更大步长来代替池化作用。不过,目前大多数卷积神经网络模型还在继续使用池化层。
卷积神经网络的学习过程
卷积神经网络的学习过程和传统神经网络类似,核心还是通过反向传播算法来优化网络参数,以实现最小化预测值与实际值之间的误差。
卷积神经网络的结构主要由重复的卷积层、激活函数、池化层组成,在最后会将最后一个池化层输出的多维特征图转换成一维特征向量,这个过程称为展平。然后接入全连接层神经网络层,最后通常会接入一个特殊函数(如 Softmax 进行归一化处理后接入激活函数进行分类)后连接到输出层。
其学习过程大致可以描述为三个环节的迭代:
- 正向传播:输入数据(一般是图像)到神经网络之中,通过多个“卷积层+激活函数+池化层”和全连接层后输出一个预测值。
- 计算损失:通过损失函数计算预测值与真实值之间的误差,返回一个损失值。常用的损失函数为交叉熵(Cross Entropy)损失函数。它的优点是可以实现对模型的预测“打分”,特点是模型越是“自信的犯错”,交叉熵损失函数“扣分”就越高。
- 反向传播:反向逐层计算损失函数对每一层参数的梯度。值得注意的是在卷积神经网络中卷积核的数值也是可以计算的参数,并不是每个卷积核都是人为设定好来提取固定特征的。在反向传播过程中,会进行参数的调整,这里涉及到一个超参数,学习率(Learning Rate,LR)。这是一个关键的超参数,控制每次参数更新的步长大小。太小会导致学习缓慢,太大可能导致震荡甚至发散。
而在 PyTorch 框架下,上述复杂的过程已经被整合成一个个可以直接调用的函数,大大提高了代码学习效率。
PyTorch
PyTorch的模型训练流程
- 数据的准备与预处理。
- 模型的搭建。
- 确定损失函数与优化器。
- 循环训练。
- 模型的评估。
模型的搭建与核心基类 nn.Module
我们在阅读各种基于 PyTorch 的深度学习代码时,经常会看到以下两句
import torch
import torch.nn as nn
torch 我们都知道,指的就是我们的 PyTorch 库,将这个库导入进来我们才能正常调用方法。但是这个 torch.nn 在 torch 中扮演的是什么角色呢?
nn,也就是 Neural Network 的缩写,中文名神经网络,torch.nn
是 PyTorch 中用于构建和训练神经网络的核心模块,为我们提供了许多封装好的模块:
- 容器类(Containers):用来组织复杂的神经网络架构。
- 预定义层:包括线性层、卷积层、池化层……
- 激活函数:包括 SIgmoid、ReLU……
- 损失函数:包括交叉熵损失函数(CrossEntropyLoss)、MSELoss……
其中的容器类中,有这样一个关键的类 nn.Module 基类。官方文档给了这样一句介绍:
我们创建的所有神经网络也都应该继承这个类,而它也提供了十分便利的功能:
- 模型管理:包括模型的加载、保存,以及模型层次结构的规范。
- 数据管理:可以自动跟踪模型的可训练参数。
- 设备迁移:
.to(device)
自动迁移参数,可以把模型放到 GPU 上面训练。
同时,结合优化器(torch.optim
)可以干净利落的完成模型的数据更新。
于是,结合 torch.nn 提供的各种方法与上文提到的卷积神经网络的学习过程,我们便可以尝试编写出如下代码。更加具体的内容(包括每一行代码的作用和使用方法)将会在下一篇笔记中呈现。
PyTorch下卷积神经网络的代码实现(以LeNet-5为例)
完整代码已经上传到 Github 上,点此链接跳转。
LeNet-5 是由 Yann LeCun 等人在 1998 年提出的,用于手写数字识别,在当时主流的激活函数还是 Sigmoid 函数且采用的池化策略是平均池化。
class Net(nn.Module):
def __init__(self):
super(Net,self).__init__()
self.model = nn.Sequential(
nn.Conv2d(in_channels=1,out_channels=6,kernel_size=5,stride=1,padding=0),
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=(2,2)),
nn.Conv2d(in_channels=6,out_channels=16,kernel_size=5,stride=1,padding=0),
nn.Sigmoid(),
nn.AvgPool2d(kernel_size=(2,2)),
nn.Sigmoid(),
nn.Linear(in_features=16*5*5,out_features=1*120),
nn.Sigmoid(),
nn.Linear(in_features=1*120,out_features=84),
nn.Sigmoid(),
nn.Linear(in_features=84,out_features=10)
)
def forward(self,x):
x = self.model(x)
return x
通过以上代码,我们构建出了 LeNet-5 手写数字识别的神经网络,而导入数据进行训练的代码同样简单。
LR = 1e-2
net = Net()
loss_fn = nn.CrossEntropyLoss()
optim = optim.SGD(net.parameters(),LR)
这四行代码决定了计算损失和反向回归参数优化的策略,我们设定了 1e-2 的学习率和交叉熵作为损失函数。
def train(train_loader):
for index,data in enumerate(train_loader):
inputs, labels = data
outputs = net(inputs)
loss = loss_fn(outputs,labels)
optim.zero_grad()
loss.backward()
optim.step()
我们生成了一个模型训练的方法,接下来只需要调用该方法时传入训练集就可以开始训练了。而其中有三个关键方法:
optim.zero_grad()
作用是清空梯度,也就是初始化的“调零”操作。loss.backward()
作用是反向传播计算梯度。optim.step()
作用通过梯度逐层更新模型参数。
仅通过调用以上三个方法,我们就可以实现整个反向传播和更新参数的过程。而更加具体的内容(包括每一行代码的作用和使用方法)将会在下一篇笔记中呈现(二次强调)。
其他卷积神经网络模型
随着深度学习的发展,各种CNN模型如雨后春笋一般出现,从最初的 LeNet5 到现在的 AlexNet、Google Inception Net、ResNet、EfficientNet 等,可见卷积神经网络已经较为成熟。
其中 AlexNet 由 Alex Krizhevs 提出,该模型采用了 ReLU 激活函数和 Dropout 策略。其中的 ReLU 激活函数现在仍在大量使用,非常有效提高模型的非线性表达能力;而 Dropout 策略则模拟实现了削弱神经元之间的互相依赖性,一定程度上抑制了模型的过拟合。