本文正在参加「金石计划」
上一篇文章我们简单介绍了 KNN、线性回归在计算机视觉中的使用,相信大家对机器学习的“从数据中学习”的思路有了一定的认识,接下来我们切入到神经网络中去。
神经网络
神经网络简介
神经网络是深度学习算法的核心。 其名称和结构均受到人脑的启发。
神经网络由节点层组成,包含一个输入层、一个或多个隐藏层和一个输出层。 每个节点也称为一个人工神经元,它们连接到另一个节点,具有相关的权重和阈值。 如果任何单个节点的输出高于指定的阈值,那么该节点将被激活,并将数据发送到网络的下一层。 否则该节点不将数据传递到网络的下一层。
神经网络依靠训练数据来学习和随时间推移提高自身精度,这些学习算法经过精度调优后就会成为计算机科学和人工智能领域中的强大工具,可支持我们快速进行数据分类和分组。
神经网络的工作流程
每个单独的节点都进行“用输入数据、权重、阈值确定该节点的输出”这一工作,具体方法如下:
- 得到输入数据后,将输入数据乘对应的权重并累加。
- 通过激活函数传递输出,如果输出超过给定阈值(或者通过某种 check),则激活该节点,并将数据传递到网络中的下一层。
- 该节点的输出成为下一个节点的输入的一部分。
将数据从一层传递至下一层的这一过程可将此神经网络定义为前馈网络。
通过调整权重或阈值,可以从模型中得出不同的结果。模型会通过反向传播并使用梯度下降法调整其权重和偏差,寻找损失函数的最小值。
卷积神经网络
之前提到的神经网络可以说是线性分类器的堆叠,只不过在中间加入非线性函数,对中间层产生的模板加权后得到最终的得分。计算机视觉中用到更多的神经网络结构是卷积神经网络。接下来我们对卷积神经网络加以介绍。
什么是卷积神经网络
卷积神经网络(Convolutional Neural Network,CNN)是一种用于提取图像不同层次特征的多层神经网络,最早由Yann LeCun提出并应用在手写字体识别中(MINST)。
卷积神经网络基本机制基于生物学上感受野,其主要思想为:只有视网膜上的特定区域(感受野)才能激活视觉神经元。
卷积神经网络主要应用于图像和视频分析任务中,比如图像分类、人脸识别、物体识别、图像分割等,其准确率远远超出了传统浅层神经网络。
接下来我们紧扣“只有感受野才能激活视觉神经元”这一核心思想对卷积神经网络的算法细节进行介绍。
卷积神经网络的详细介绍
典型的卷积神经网络由输入层、卷积层、激活层、池化层、全连接层与输出层组成。
即 INPUT-CONV-RELU-POOL-FC。
其中,卷积层、激活层与池化层通常组成多个卷积组以提取图像不同层次的特征,最终通过若干个全连接层进行分类。
输入层
卷积神经网络可对灰度与 RGB 两类数字图像进行处理。
对于灰度图像,其像素取值范围为[0,255](“0”表示纯黑、“255”表示纯白、其间不同数字表示不同层次的灰色),神经网络中的输入可视为一个 二维数字矩阵(M与N分别表示图像行数与列数)。
对于 RGB 图像,由于其像素颜色由红(R)、绿(G)、蓝(B)三原色的组合得到,因而在结构上由三个不同颜色分量的层次组成,神经网络的输入可视为一个 三维数字矩阵。
卷积层
卷积层是构建卷积神经网络的核心层,网络中大部分的计算量都由它产生。卷积层主要用来提取不同层次的特征。
卷积层的参数是由一些可学习的滤波器(filter) 集合构成的。每个滤波器在宽度和高度上都比较小,但是深度和输入数据一致。
如下图所示,输入图像尺寸为 ,卷积核尺寸为 (5×5为由长度与宽度决定的感受野尺寸、3为与输入图像通道或颜色分量层次、2为卷积核数量),输入图像通过卷积运算后将得到 个 的特征图。
这个过程是通过卷积操作实现的。卷积就是对图像的不同局部矩阵和卷积核矩阵各个位置的元素相乘再相加运算。
如下图所示,假设图像为二维 的矩阵,而卷积核是一个 的矩阵。如果按逐像素(每次移动一个像素)卷积规则:
则首先对输入图左上角 局部矩阵和卷积核矩阵进行卷积(即各个位置的元素相乘再相加)以得到输出矩阵 的 元素,其值为 ;然后卷积核向右平移一个像素以对(b,c,f,g)四个元素构成的矩阵进行卷积运算,进而可得到输出矩阵 的 元素值;同理可得出矩阵 的 、、 与 的值。最终矩阵 即为得到的输出矩阵。
卷积通过以上方式对图像特征进行提取,不同的卷积核能够提取不同的图像特征,卷积后的输出矩阵为“特征图像”。
不同类型的卷积核对图像进行卷积后,可提取图像边缘、棱角、模糊、突出等图像特征的能力。不同层次的卷积层可提取输入图像不同层次的特征;低层卷积层偏向于提取图像底层、局部特征(如物体边缘),而高层卷积层则可提取包含更丰富语义信息的全局特征(如物体结构)。
激活层
卷积层主要对图像进行线性运算,对于非线性运算问题,通常需要利用激活层对卷积层的输出进行非线性映射。激活函数可以引入非线性因素,主要作用是提供神经网络的非线性建模能力。常用的激活函数如下图所示。
池化层
“池化”,即“下采样”,其目的在于:
- 对特征图进行压缩处理以降低计算复杂度或减小模型的规模。
- 突出主要特征或提高特征的鲁棒性。
- 在一定程度上控制过拟合。
池化操作包括平均池化与最大池化两种;如图所示,最大池化(Max Pooling)操作在特征图中求取 区域内值的最大者作为原 区域内值的表达,最终可得到右图所示的结果(步长为 )。平均池化与之类似顾名思义。当然,窗口大小是人工设置的,除此之外,池化层没有任何参数需要设置或训练。
全连接层
全连接层将最后的特征输出映射到线性可分的空间以完成分类或回归任务。即将获得的卷积(包括激活与池化等处理)结果转换为向量输入至分类器(如 Softmax 分类器)进行分类。
代码实现
导包
import torch
from torch import nn, optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import torch.nn.functional as F
import matplotlib.pyplot as plt
生成数据
batch_size = 64
#预处理设置
data_tf = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize([0.5], [0.5])])
# 生成数据
train_dataset = datasets.MNIST(root='./data', train=True, transform=data_tf, download=True)
test_dataset = datasets.MNIST(root='./data', train=False, transform=data_tf)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
定义卷积神经网络
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(1, 20, 5, 1) # 28+1-5 = 24
self.conv2 = nn.Conv2d(20, 50, 5, 1)
self.fc1 = nn.Linear(4*4*50, 500)
self.fc2 = nn.Linear(500, 10)
def forward(self, x):
# x: 1 * 28 * 28
x = F.relu(self.conv1(x)) # 24 * 24 * 20
x = F.max_pool2d(x, 2, 2) # 12 * 12 * 20
x = F.relu(self.conv2(x)) # 8 * 8 * 50
x = F.max_pool2d(x, 2, 2) # 4 * 4 * 50
x = x.view(-1, 4*4*50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
# 实例化模型
net = CNN()
训练
# 定义损失函数
Loss = nn.CrossEntropyLoss()
# 设置优化器
Opt = optim.SGD(net.parameters(), lr=0.02)
# 训练模型
T = 10
for epoch in range(T):
Loss_Sum = 0.0 #累积训练误差
Acc_Sum = 0.0 #累积训练精度
for i, data in enumerate(train_loader, 1):
img, label = data
img = Variable(img)
label = Variable(label)
label_pred = net(img) #前向传播
L = Loss(label_pred, label) #计算误差
Loss_Sum += L.data.numpy() #误差累积
_, label_opt = torch.max(label_pred, 1) #求取预测概率对应的类别
Acc_Sum += (label_opt == label).float().mean() #累积精度
Opt.zero_grad() #梯度清零
L.backward() #反向传播
Opt.step() #更新参数
#显示误差与精度变化
if (epoch==0) | ((epoch+1) % 2 == 0):
print('Epoch:[{}/{}], Loss: {:.4f}, Accuracy: {:.4f}'.format(epoch+1, T, Loss_Sum / i, Acc_Sum / i))
测试
net.eval()
Acc_Sum = 0.0 #累积精度
for i, data in enumerate(test_loader, 1):
img, label = data
img = Variable(img)
label_pred = net(img)
_, label_opt = torch.max(label_pred, 1)
Acc_Sum += (label_opt == label).float().mean()
print('Accuracy: {:.4f}'.format(Acc_Sum / i))