看完这篇我就不信还有人不懂卷积神经网络!
前言
在深度学习大🔥的当下,我知道介绍卷积神经网络的文章已经在全网泛滥,但我还是想要写出一点和别人不一样的东西,尽管要讲的知识翻来覆去还是那么一些,但我想尽可能做到极其通俗易懂,只要稍微有点计算机和线性代数基础的同学都能看懂。
水平有限,但凡有错误或理解不到位的地方,欢迎各位大佬指出🙏
什么是神经网络?
在介绍卷积神经网络之前,我们先回顾一下神经网络的基本知识📖。就目前而言,神经网络是深度学习算法的核心,我们所熟知的很多深度学习算法的背后其实都是神经网络。神经网络由节点层组成,通常包含一个输入层、一个或多个隐藏层和一个输出层,我们所说的几层神经网络通常指的是隐藏层的个数,因为输入层和输出层通常是固定的。节点之间相互连接并具有相关的权重和阈值。如果节点的输出高于指定的阈值,则激活该节点并将数据发送到网络的下一层。否则,没有数据被传递到网络的下一层。关于节点激活的过程有没有觉得非常相似?没错,这其实就是生物的神经元产生神经冲动的过程的模拟。
神经网络类型多样,适用于不同的应用场景。例如,循环神经网络(RNN
)通常用于自然语言处理和语音识别,而卷积神经网络(ConvNets
或 CNN
)则常用于分类和计算机视觉任务。在 CNN
产生之前,识别图像中的对象需要手动的特征提取方法。现在,卷积神经网络为图像分类和对象识别任务提供了一种更具可扩展性的方法,它利用线性代数的原理,特别是矩阵乘法,来识别图像中的模式。但是,CNN
的计算要求很高,通常需要图形处理单元 (GPU
) 来训练模型,否则训练速度很慢。
什么是卷积神经网络?
卷积神经网络是一种基于卷积计算的前馈神经网络。与普通的神经网络相比,它具有局部连接、权值共享等优点,使其学习的参数量大幅降低,且网络的收敛速度也更快。同时,卷积运算能更好地提取图像的特征。卷积神经网络的基本组件有卷积层、池化层和全连接层。卷积层是卷积网络的第一层,其后可以跟着其他卷积层或池化层,最后一层是全连接层。越往后的层识别图像越大的部分,较早的层专注于简单的特征,例如颜色和边缘。随着图像数据在 CNN
的各层中前进,它开始识别物体的较大元素或形状,直到最终识别出预期的物体。
下面将简单介绍这几种基本组件的相关原理和作用。
卷积层
卷积层是 CNN
的核心组件,其作用是提取样本的特征。它由三个部分组成,即输入数据、过滤器和特征图。在计算机内部,图像以像素矩阵的形式存储。若输入数据是一个RGB
图像,则由 3D 像素矩阵组成,这意味着输入将具有三个维度——高度、宽度和深度。过滤器,也叫卷积核、特征检测器,其本质是一个二维(2-D)权重矩阵,它将在图像的感受野中移动,检查特征是否存在。
卷积核的大小不一,但通常是一个 3x3 的矩阵,这也决定了感受野的大小。不同卷积核提取的图像特征也不同。从输入图像的像素矩阵的左上角开始,卷积核的权重矩阵与像素矩阵的对应区域进行点积运算,然后移动卷积核,重复该过程,直到卷积核扫过整个图像。这个过程就叫做卷积。卷积运算的最终输出就称为特征图、激活图或卷积特征。
如上图所示☝,特征图中的每个输出值不必连接到输入图像中的每个像素值,它只需要连接到应用过滤器的感受野。由于输出数组不需要直接映射到每个输入值,卷积层(以及池化层)通常被称为“部分连接”层。这种特性也被描述为局部连接。
当卷积核在图像上移动时,其权重是保持不变的,这被称为权值共享。一些参数,如权重值,是在训练过程中通过反向传播和梯度下降的过程进行调整的。在开始训练神经网络之前,需要设置三个影响输出体积大小的超参数:
- 过滤器的数量 :影响输出的深度。例如,三个不同的过滤器将产生三个不同的特征图,从而产生三个深度。
- 步长(Stride) :卷积核在输入矩阵上移动的距离或像素数。虽然大于等于 2 的步长很少见,但较大的步长会产生较小的输出。
- 零填充(Zero-padding) :通常在当过滤器不适合输入图像时使用。这会将输入矩阵之外的所有元素设置为零,从而产生更大或相同大小的输出。有三种类型的填充:
- Valid padding:这也称为无填充。在这种情况下,如果维度不对齐,则丢弃最后一个卷积。
- Same padding:此填充确保输出层与输入层具有相同的大小。
- Full padding:这种类型的填充通过在输入的边界添加零来增加输出的大小。
在每次卷积操作之后,CNN
的特征图经过激活函数(Sigmoid
、ReLU
、Leaky ReLU
等)的变换,从而对输出做非线性的映射,以提升网络的表达能力。
当 CNN
有多个卷积层时,后面的层可以看到前面层的感受野内的像素。例如,假设我们正在尝试确定图像是否包含自行车。我们可以把自行车视为零件的总和,即由车架、车把、车轮、踏板等组成。自行车的每个单独部分在神经网络中构成了一个较低级别的模式,各部分的组合则代表了一个更高级别的模式,这就在 CNN
中创建了一个特征层次结构。
池化层
为了减少特征图的参数量,提高计算速度,增大感受野,我们通常在卷积层之后加入池化层(也称降采样层)。池化能提高模型的容错能力,因为它能在不损失重要信息的前提下进行特征降维。这种降维的过程一方面使得模型更加关注全局特征而非局部特征,另一方面具有一定的防止过拟合的作用。池化的具体实现是将感受域中的值经过聚合函数后得到的结果作为输出。池化有两种主要的类型:
- 最大池化(Max pooling):当过滤器在输入中移动时,它会选择具有最大值的像素发送到输出数组。与平均池化相比,这种方法的使用频率更高。
- 平均池化(Average pooling):当过滤器在输入中移动时,它会计算感受域内的平均值以发送到输出数组。
全连接层
全连接层通常位于网络的末端,其结构正如其名。如前所述,输入图像的像素值在部分连接层中并不直接连接到输出层。但是,在全连接层中,输出层中的每个节点都直接连接到前一层中的节点,特征图在这里被展开成一维向量。
该层根据通过前几层提取的特征及其不同的过滤器执行分类任务。虽然卷积层和池化层倾向于使用 ReLu
函数,但 FC 层通常用 Softmax
激活函数对输入进行适当分类,产生 [0, 1]
之间的概率值。
自定义卷积神经网络进行手写数字识别
- 导包
python复制代码import time
import numpy as np
import torch
import torch.nn.functional as F
from torchvision import datasets
from torchvision import transforms
from torch.utils.data import DataLoader
if torch.cuda.is_available():
torch.backends.cudnn.deterministic = True
- 设置参数 & 加载数据集
ini复制代码##########################
### SETTINGS
##########################
# Device
device = torch.device("cuda:3" if torch.cuda.is_available() else "cpu")
# Hyperparameters
random_seed = 1
learning_rate = 0.05
num_epochs = 10
batch_size = 128
# Architecture
num_classes = 10
##########################
### MNIST DATASET
##########################
# Note transforms.ToTensor() scales input images
# to 0-1 range
train_dataset = datasets.MNIST(root='data',
train=True,
transform=transforms.ToTensor(),
download=True)
test_dataset = datasets.MNIST(root='data',
train=False,
transform=transforms.ToTensor())
train_loader = DataLoader(dataset=train_dataset,
batch_size=batch_size,
shuffle=True)
test_loader = DataLoader(dataset=test_dataset,
batch_size=batch_size,
shuffle=False)
# Checking the dataset
for images, labels in train_loader:
print('Image batch dimensions:', images.shape)
print('Image label dimensions:', labels.shape)
break
- 模型定义
ini复制代码##########################
### MODEL
##########################
class ConvNet(torch.nn.Module):
def __init__(self, num_classes):
super(ConvNet, self).__init__()
# calculate same padding:
# (w - k + 2*p)/s + 1 = o
# => p = (s(o-1) - w + k)/2
# 28x28x1 => 28x28x8
self.conv_1 = torch.nn.Conv2d(in_channels=1,
out_channels=8,
kernel_size=(3, 3),
stride=(1, 1),
padding=1) # (1(28-1) - 28 + 3) / 2 = 1
# 28x28x8 => 14x14x8
self.pool_1 = torch.nn.MaxPool2d(kernel_size=(2, 2),
stride=(2, 2),
padding=0) # (2(14-1) - 28 + 2) = 0
# 14x14x8 => 14x14x16
self.conv_2 = torch.nn.Conv2d(in_channels=8,
out_channels=16,
kernel_size=(3, 3),
stride=(1, 1),
padding=1) # (1(14-1) - 14 + 3) / 2 = 1
# 14x14x16 => 7x7x16
self.pool_2 = torch.nn.MaxPool2d(kernel_size=(2, 2),
stride=(2, 2),
padding=0) # (2(7-1) - 14 + 2) = 0
self.linear_1 = torch.nn.Linear(7*7*16, num_classes)
for m in self.modules():
if isinstance(m, torch.nn.Conv2d) or isinstance(m, torch.nn.Linear):
m.weight.data.normal_(0.0, 0.01)
m.bias.data.zero_()
if m.bias is not None:
m.bias.detach().zero_()
def forward(self, x):
out = self.conv_1(x)
out = F.relu(out)
out = self.pool_1(out)
out = self.conv_2(out)
out = F.relu(out)
out = self.pool_2(out)
logits = self.linear_1(out.view(-1, 7*7*16))
probas = F.softmax(logits, dim=1)
return logits, probas
torch.manual_seed(random_seed)
model = ConvNet(num_classes=num_classes)
model = model.to(device)
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
- 模型训练
scss复制代码def compute_accuracy(model, data_loader):
correct_pred, num_examples = 0, 0
for features, targets in data_loader:
features = features.to(device)
targets = targets.to(device)
logits, probas = model(features)
_, predicted_labels = torch.max(probas, 1)
num_examples += targets.size(0)
correct_pred += (predicted_labels == targets).sum()
return correct_pred.float()/num_examples * 100
start_time = time.time()
for epoch in range(num_epochs):
model = model.train()
for batch_idx, (features, targets) in enumerate(train_loader):
features = features.to(device)
targets = targets.to(device)
### FORWARD AND BACK PROP
logits, probas = model(features)
cost = F.cross_entropy(logits, targets)
optimizer.zero_grad()
cost.backward()
### UPDATE MODEL PARAMETERS
optimizer.step()
### LOGGING
if not batch_idx % 50:
print ('Epoch: %03d/%03d | Batch %03d/%03d | Cost: %.4f'
%(epoch+1, num_epochs, batch_idx,
len(train_loader), cost))
model = model.eval()
print('Epoch: %03d/%03d training accuracy: %.2f%%' % (
epoch+1, num_epochs,
compute_accuracy(model, train_loader)))
print('Time elapsed: %.2f min' % ((time.time() - start_time)/60))
print('Total Training Time: %.2f min' % ((time.time() - start_time)/60))
- 模型评估
python复制代码with torch.set_grad_enabled(False): # save memory during inference
print('Test accuracy: %.2f%%' % (compute_accuracy(model, test_loader)))
输出如下👇
Test accuracy: 97.97%