3.6 神经网络
3.6.1 原理概述
神经网络在机器学习中的地位不断上升,尤其是基于深度神经网络的分类识别算法超越了很多早期的机器学习算法,神经网络的基本运算包括:1)全连接层运算;2)卷积运算;3)池化运算;4)非线性激活函数运算。下面分别介绍。
1.全连接层运算
我们这里讨论的全连接层只包括线性部分,而激活函数单独放在后面讨论。全连接层的线性运算如图3-9所示。
图3-9 神经网络全连接层输入和输出连接关系示意图
它对应的具体运算公式是下面形式的矩阵运算:
其中x是1×N的输入数据向量、y是1×M的输出数据向量,b是1×N的偏置向量,W是M×N的权重系数矩阵。
2.卷积运算
神经网络中的卷积运算主要是二维卷积,它可以看成滑动窗口在需要卷积的特征数据上移动,在每个移动位置计算窗口内元素的加权和,如图3-10所示。
图3-10 二维卷积运算示意图
卷积结果和待卷积数据以及卷积核的关系为
在很多神经网络软件框架中,卷积运算被转换成矩阵乘法实现,下面通过一个简单的例子说明。图3-11给出了一个二维卷积的例子。
图3-11 二维卷积的计算例子
图3-11中给出的4个卷积结果对应的运算为
上面的运算是线性运算,可以写成下面的矩阵形式:
3.池化运算
池化运算是对特征数据进行“降采样”,对于二维特征数据进行池化运算的过程可以看成使用给定尺寸的窗口(后面称之为“滑动窗口”)在数据沿二维特征数据“矩阵”中按特定步长滑动,在滑动到的每个位置,对滑动窗口内所有特征数据进行降采样。其中降采样的方式有多种,常见的包括:最大/最小池化——取所有窗口内数据的最大值或最小值;平均池化——取所有元素的平均值。应用过程中也可以使用其他降采样方法,比如简单的抽样,抽取滑动窗口中心的一个元素,或者使用去除最大最小值后的平均值等。池化运算主要是滑动窗口元素的比较和累加。图3-12是池化运算示例的示意图。
图3-12 池化运算示例,滑动窗口尺寸为3×3,滑动步长是3,在滑动窗口4种可能的位置处合并窗口内的数据,得到尺寸为2×2的输出数据
4.非线性激活函数运算
激活函数可以看成一个映射,将输入数据z转成输出g(z)。在神经网络中,g(z)的常见形式包括Sigmoid激活函数、tanh激活函数、ReLU激活函数、Leaky ReLU激活函数等。图3-13中给出了典型激活函数的输入输出关系的公式和示意图。
图3-13 几种常用的激活函数表达式和输入输出关系曲线
3.6.2 模型训练和推理
下面给出手写数字识别的卷积神经网络的训练和推理的代码示例,所提供的例程基于Pytorch框架。手写数字识别任务是机器学习领域使用最广泛的示例之一,它的数据集是用灰度图表示的不同人书写的数字,每个图片尺寸是28×28,图3-14给出部分数字的图像。完整的数据能够从网站http://yann.lecun.com/exdb/mnist/下载得到。数据以特定格式压缩文件形式提供,在网站中有详细的格式描述。在Python训练例程中,我们为了使用方便,已经将其转成Numpy能够直接识别的*.npy格式存储。
图3-14 手写数字识别应用中的图片样本
·神经网络结构
为了识别其中的数字,我们构建的神经网络由两个卷积层和两个全连接层构成,网络结构和各层运算的输入输出数据尺寸如图3-15所示。
图3-15 手写数字识别神经网络的结构和各层数据尺寸
神经网络运算流程的描述如下:
1)第一个卷积层使用32个5×5的卷积核对28×28原始图片进行卷积,得到32个24×24的卷积结果,经过ReLU激活函数运算并池化后,得到32个12×12的特征图。
2)第二个卷积层使用32个32通道的5×5卷积核,作用于上一层数据得到32个8×8的特征图,经过ReLU和池化后得到32个4×4特征图。
3)第二卷积层处理结果被“拉直”成512维(512=32×4×4)的向量。
4)第一个全连接层,该层输出1024维的向量,输出同样经过ReLU函数运算。
5)第二个全连接层,该层输出10维向量,作为10个数字类型的匹配“得分”。其中“得分”最高的元素对应于原始图像对应的最可能的数字。
上述神经网络通过现成的神经网络框架能够高效地构建和训练。代码清单3-6所示是基于Pytorch的神经网络构建的代码。
代码清单3-6 卷积神经网络类的例子
## 网络 class mnist_c(nn.Module): def __init__(self): super(mnist_c, self).__init__() self.conv1 = nn.Conv2d( 1, 32, 5, 1) self.conv2 = nn.Conv2d(32, 32, 5, 1) self.dropout = nn.Dropout2d(0.4) self.fc1 = nn.Linear(512, 1024) self.fc2 = nn.Linear(1024, 10) # 浮点训练和推理 def forward(self,x): x = self.conv1(x) x = F.relu(x) x = F.max_pool2d(x, 2) x = self.conv2(x) x = F.relu(x) x = F.max_pool2d(x, 2) x = torch.flatten(x, 1) x = self.fc1(x) x = F.relu(x) x = self.dropout(x) x = self.fc2(x) return x
上面的代码中,函数forward定义了神经网络的具体运算,相比之前的图3-15,上面的代码多了一个dropout运算(倒数第三行),这一运算用于在训练过程中将部分运算结果置零,这样能够提高神经网络训练效果,在神经网络推理运算中,dropout层的运算被直接跳过。
·神经网络训练
对于上述神经网络,我们使用有监督训练,即提供一系列图片和人工标注的正确答案,其中是训练图片,是正确答案。神经网络输出的10维向量可以看作输入图片分别属于10个数字的得分。训练过程就是不断调整神经参数(记作θ,在这个示例中指卷积层的卷积核和偏置,以及全连接层的权重和偏置),使得神经网输出的10个数字的得分最大值对应参考答案。对于这一类分类问题,通常使用交叉熵代价函数,定义为
其中θ代表神经网络的可调参数,是第n个训练数据送入网络后,输出10维向量的经过softmax运算(参考式(7-11))后的第个元素的值。训练过程不断更新神经网络参数θ的值,使代价函数尽可能小。对于上面给出的网络训练过程,其代码如代码清单3-7所示。
代码清单3-7 卷积神经网络的训练例程
def train(args, model, device, train_loader, test_loader): model.to(device) # model是神经网络对象 # 网络参数优化模块(输入参数lr是学习率) optimizer = optim.Adadelta(model.parameters(), lr=args.lr) # 学习率调节模块 scheduler = StepLR(optimizer, step_size=1, gamma=args.gamma) # 逐轮训练 for epoch in range(1, args.epochs + 1): model.train() # 逐批取出训练数据执行训练 for batch_idx, (data, target) in enumerate(train_loader): # 数据格式转换 data = data.resize_(args.batch_size, 1, 28, 28) data, target = data.to(device), target.to(device) # 清除之前的梯度计算结果 optimizer.zero_grad() # 由网络model计算输入数据data,得到输出output output = model(data) # 计算神经网络输出的误差损失函数, # 并通过反向传播算法计算它对神经网络参数的导数 loss = F.cross_entropy(output, target) loss.backward() # 执行优化,根据误差损失更新网络参数 optimizer.step() # 更新学习率 scheduler.step() device = torch.device("cuda") # 使用NVIDIA的GPU执行训练 model = mnist_c() # 生成神经网络 # 调用函数train,执行网络训练 train(args, model, device, train_loader,test_loader)
上述代码有详细的注释解释各行原理。其中API的参数含义和用法细节需要读者阅读torch的用户使用文档,这里限于篇幅不具体展开。该神经网络经过训练能够达到高于99%的分类精度。注意,具体的性能和训练时使用的超参数以及参数初始化的随机数“种子”有关,修改这些参数可能得到不同的性能。
训练完成了的网络通过下面的Python代码实现前向推理运算:
output = model(data) pred = output.argmax(dim=1, keepdim=True)
其中data是需要识别的图像数据,格式是1×1×28×28的高维数组,output是前向推理输出,对output的成员函数argmax调用返回output中最大元素的序号,pred对应0~9这10个离散值,即网络识别出的手写数字的值。
此处给出的神经网络内部各个运算模块的原理介绍是基本的和概念性的,在第7章我们会给出更详细的介绍,包括每个运算的具体结构和代码实现。