一、前言

  通过上一篇文章,我们大概了解了卷积是什么,并且分析了为什么卷积能在图像识别上起到巨大的作用。接下来,废话不多话,我们自己尝试动手搭建一个简易的CNN网络。

二、准备工作

  在开始的时候,我们首先概括一下卷积所需要进行的工作:

  1. 定义一个卷积核:卷积核是一个小的矩阵(例如3x3或5x5),包含一些数字。这个卷积核的作用是在图像中识别特定类型的特征,例如边缘、线条等,也可能是难以描述的抽象特征。
  2. 卷积核滑过图像:卷积操作开始时,卷积核会被放置在图像的左上角。然后,它会按照一定的步长(stride)在图像上滑动,可以是从左到右,也可以是从上到下。步长定义了卷积核每次移动的距离。
  3. 计算点积:在卷积核每个位置,都会计算卷积核和图像对应部分的点积。这就是将卷积核中的每个元素与图像中对应位置的像素值相乘,然后将所有乘积相加。
  4. 生成新的特征图:每次计算的点积结果被用来构建一个新的图像,也称为特征图或卷积图。
  5. 重复以上过程:通常在一个 CNN 中,我们会有多个不同的卷积核同时进行卷积操作。这意味着我们会得到多个特征图,每个特征图捕捉了原始图像中的不同特征。

  上图中间的矩阵就是所谓的卷积核,又称为滤波器。这个滤波器可以帮助我们观测到一定区域大小的像素信息,并且通过卷积计算,变成”低频“的信息特征,比如上面我们提到的一些图像的边缘,纹理等等。当权重系数(卷积核)的参数改变时,它可以提取的特征类型也会改变。所以训练卷积神经网络时,实质上训练的是卷积核的参数。如下图所示,是一次卷积计算的过程:

  一个内核从图像的左上角开始滑动,将核所覆盖的像素值与相应的核值相乘,并对乘积求和。结果被放置在新图像中与核的中心相对应的点处。上面的235其实就是计算所得出的灰度值,当一个内核走完整个图片之后,得出的结果大概是这样子:

三、CNN架构

  当只有一层CNN结构时,一般都会有如下几个层级,来帮助我们进行一次卷积,训练对应的特征。

  • 输入层(Input Layer)

    输入层负责接收原始数据,例如图像。每个节点对应输入数据的一个特征。

  • 卷积层(Convolutional Layer)

    卷积层是CNN的核心。它通过应用卷积操作来提取图像中的特征。每个卷积层包含多个卷积核(也称为滤波器),每个卷积核负责检测输入中的不同特征。卷积操作通过滑动卷积核在输入上进行计算,并生成特征图。

  • 激活函数层(Activation Layer)

    在卷积层之后,一般会添加激活函数,例如ReLU(Rectified Linear Unit),用于引入非线性性。这有助于模型学习更复杂的模式和特征。

  • 池化层(Pooling Layer)

    池化层用于减小特征图的空间维度,降低计算复杂度,并减少过拟合风险。常见的池化操作包括最大池化和平均池化。

  • 全连接层(Fully Connected Layer)

    全连接层将前面层的所有节点与当前层的所有节点连接。这一层通常用于整合前面层提取的特征,并生成最终的输出。在分类问题中,全连接层通常输出类别的概率分布。

  • 输出层(Output Layer)

    输出层给出网络的最终输出,例如分类的概率分布。通常使用softmax函数来生成概率分布。

  输入层就不用解释了,毕竟在全连接网络中我们已经对它有了一定的了解,我们首先看看在卷积层中,具体是怎么实现的。在我们接下来要做的 MNIST 手写数字识别的数据集中,我们用其中的图片来举例,例如 8 这个图片:

  在卷积层中左边黑色为原图,中间为卷积层,右边为卷积后的输出。我们可以注意到,当我们是不用的卷积核时,所对应的结果是不同的。

  

  上面第一种卷积核所有元素值相同,所以它可以计算输入图像在卷积核覆盖区域内的平均灰度值。这种卷积核可以平滑图像,消除噪声,但会使图像变得模糊。第二种卷积核可以检测图像中的边缘,可以看到输入的8的边缘部分颜色更深一些,在更大的图片中这种边缘检测的效果会更明显。

  需要注意的是,虽然上边说道不同的卷积核有着不同的作用,但是在卷积神经网络中,卷积核并不是手动设计出来的,而是通过数据驱动的方式学习得到的。这就是说,我们并不需要人工设计出特定的卷积核来检测边缘、纹理等特定的特征,而是让模型自己从训练数据中学习这些特征,即模型可以自动从复杂数据中学习到抽象和复杂的特征,这些特征可能人工设计难以达到。

  在卷积过程中需要注意的两个参数是:步长和零填充。如果两者优化得当,可以让CNN的效果更加好。

  1. 步长 - Strade 

  在卷积神经网络(CNN)中,"步长"(stride)是一个重要的概念。步长描述的是在进行卷积操作时,卷积核在输入数据上移动的距离。在两维图像中,步长通常是一个二元组,分别代表卷积核在垂直方向(高度)和水平方向(宽度)移动的单元格数。例如,步长为1意味着卷积核在每次移动时,都只移动一个单元格,这就意味着卷积核会遍历输入数据的每一个位置;同理,如果步长为2,那么卷积核每次会移动两个单元格。如下图,就是 strade=2 时,卷积后所得的结果:

   

  步长的选择会影响卷积操作的输出尺寸。更大的步长会产生更小的输出尺寸,反之同理。

  之所以设置步长,主要考虑以下几点:

  • 降低计算复杂性:当步长大于1时,卷积核在滑动过程中会"跳过"一些位置,这将减少输出的尺寸并降低后续层的计算负担。
  • 模型的可扩展性:增大步长可以有效地降低网络层次的尺寸,使得模型能处理更大尺寸的输入图片。
  • 控制过拟合:过拟合是指模型过于复杂,以至于开始"记住"训练数据,而不是"理解"数据中的模式。通过减少模型的复杂性,我们可以降低过拟合的风险。
  • 减少存储需求:更大的步长将产生更小的特征映射,因此需要更少的存储空间。

  

  2. 零填充 - Zero Padding

  注意上面的图,我们发现底部输入图片的中间十字部分,被输入了两次。并且输入图像与卷积核进行卷积后的结果中损失了部分值,输入图像的边缘被“修剪”掉了(边缘处只检测了部分像素点,丢失了图片边界处的众多信息)。这是因为边缘上的像素永远不会位于卷积核中心,而卷积核也没法扩展到边缘区域以外。这个结果我们是不能接受的,有时我们还希望输入和输出的大小应该保持一致。为解决这个问题,可以在进行卷积操作前,对原矩阵进行边界填充(Padding),也就是在矩阵的边界上填充一些值,以增加矩阵的大小,通常都用”空“来进行填充。

  如果定义输入层的边长是M,卷积核的边长是K,填充的圈数为P,以及步长的长度为S,我们可以推导出卷积之后输出的矩阵大小为:

  激活层就不解释了,一般来说引入激活层只是引入非线性,官方指导中也说明,一般使用 Relu 即可。而池化层则其实是为了让模型具有泛化的能力,其实说白了就是为了避免模型的 overfit,使用池化层让模型忘记一些之前学过的特征,这里的做法其实在后面的很多模型中都能看到。

  池化层主要采用最大池化(Max Pooling)、平均池化(Average Pooling)等方式,对特征图进行操作。以最常见的最大池化为例,我们选择一个窗口(比如 2x2)在特征图上滑动,每次选取窗口中的最大值作为输出,如下图这就是最大池化的工作方式:

  大致可以看出,经过池化计算后的图像,基本就是左侧特征图的“低像素版”结果。也就是说池化运算能够保留最强烈的特征,并大大降低数据体量。

四、手写数字CNN设计

  接下来,我们可以使用上面提到的 CNN 的各个层,给我们自己的手写数字识别设计网络架构。其实就是让 卷积层 -> Relu -> 池化层 叠加 N 层,然后最后最后再加入全连接层,进行分类。

  下图就是具体的网络架构图

  用代码实现如下:

import torch
from torch import nn
from torch.nn import functional as F
from torch import optim
import torchvision
from matplotlib import pyplot as plt
from utils import plot_curve, plot_image, one_hot, predict_plot_image # step 1 : load dataset
batch_size = 512
# https://blog.csdn.net/weixin_44211968/article/details/123739994
# DataLoader 和 dataset 数据集的应用
train_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('./data', train=True, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,)
)
])),
batch_size=batch_size, shuffle=True
) test_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('./data', train=True, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,)
)
])),
batch_size=batch_size, shuffle=False
) # step 2 : CNN网络
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
# 图片是灰度图片,只有一个通道
# 第一层
self.conv1 = nn.Conv2d(in_channels=1, out_channels=16,
kernel_size=5, stride=1, padding=2)
self.relu1 = nn.ReLU()
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2) # 第二层
self.conv2 = nn.Conv2d(in_channels=16, out_channels=32,
kernel_size=5, stride=1, padding=2)
self.relu2 = nn.ReLU()
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2) # 全连接层
self.fc1 = nn.Linear(in_features=7 * 7 * 32, out_features=256)
self.relufc = nn.ReLU()
self.fc2 = nn.Linear(in_features=256, out_features=10) # 定义前向传播过程的计算函数
def forward(self, x):
# 第一层卷积、激活函数和池化
x = self.conv1(x)
x = self.relu1(x)
x = self.pool1(x)
# 第二层卷积、激活函数和池化
x = self.conv2(x)
x = self.relu2(x)
x = self.pool2(x)
# 将数据平展成一维
x = x.view(-1, 7 * 7 * 32)
# 第一层全连接层
x = self.fc1(x)
x = self.relufc(x)
# 第二层全连接层
x = self.fc2(x)
return x # step3: 定义损失函数和优化函数
# 学习率
learning_rate = 0.005
# 定义损失函数,计算模型的输出与目标标签之间的交叉熵损失
criterion = nn.CrossEntropyLoss()
model = CNN()
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9) # step4: 模型训练
train_loss = []
# 定义迭代次数
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 将神经网络模型 net 移动到指定的设备上。
model = model.to(device)
# 为了和全连接网路的训练次数一致,我们采用相同的迭代次数
total_step = len(train_loader)
num_epochs = 3
for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader):
images = images.to(device)
labels = labels.to(device)
optimizer.zero_grad() # 清空上一个batch的梯度信息
# 将输入数据 inputs 喂入神经网络模型 net 中进行前向计算,得到模型的输出结果 outputs。
outputs = model(images)
# 使用交叉熵损失函数
loss = criterion(outputs, labels)
# 使用反向传播算法计算模型参数的梯度信息
loss.backward()
# 更新梯度
optimizer.step() train_loss.append(loss.item())
# 输出训练结果
if (i + 1) % 100 == 0:
print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'.format(epoch + 1, num_epochs, i + 1, total_step,
loss.item())) print('Finished Training')
# 打印 loss 损失图
plot_curve(train_loss) # step 5 : 准确度测试
# 测试CNN模型
with torch.no_grad(): # 进行评测的时候网络不更新梯度
correct = 0
total = 0
for images, labels in test_loader:
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: {} %'.format(100 * correct / total))

  

  损失可以看到经过两层的卷积层之后,虽然没有全连接层的Loss那么低,但是下降的还是非常快的,最后趋于平稳。

  经过三个epoch的训练,结果如下:

Epoch [1/3], Step [100/118], Loss: 0.4683
Epoch [2/3], Step [100/118], Loss: 0.2781
Epoch [3/3], Step [100/118], Loss: 0.1155
Finished Training
Accuracy of the network on the 10000 test images: 96.47 %

  我们可以惊奇的发现,这里test数据中预测的准确率竟然高达 96.47%。要知道我们使用好几层的全连接网络,最后虽然 Loss 值非常低,但是在 test 数据中的准确度都只是徘徊在 90% 左右。而我们在使用相同层数,同等训练 epoch 的情况下,我们就把识别准确率提高了7%左右,这真是令人兴奋的进步!!!

  以上就是CNN的全部实践过程了,这个系列的一小节就到此结束了,随着学习深入,我发现机器学习中比较能做出成果的是深度学习,所以这几篇都是关于深度学习的内容。后面会更多的更新机器学习相关知识,我只是知识的搬运工,下期再见~

Reference

[1] https://zhuanlan.zhihu.com/p/635438713

[2] https://mlnotebook.github.io/post/CNN1/

[3] https://poloclub.github.io/cnn-explainer/#article-convolution

[4] https://blog.csdn.net/weixin_41258131/article/details/133013757

[5] https://alexlenail.me/NN-SVG/LeNet.html

机器学习从入门到放弃:卷积神经网络CNN(二)的更多相关文章

  1. 写给程序员的机器学习入门 (八) - 卷积神经网络 (CNN) - 图片分类和验证码识别

    这一篇将会介绍卷积神经网络 (CNN),CNN 模型非常适合用来进行图片相关的学习,例如图片分类和验证码识别,也可以配合其他模型实现 OCR. 使用 Python 处理图片 在具体介绍 CNN 之前, ...

  2. 卷积神经网络(CNN)学习笔记1:基础入门

    卷积神经网络(CNN)学习笔记1:基础入门 Posted on 2016-03-01   |   In Machine Learning  |   9 Comments  |   14935  Vie ...

  3. python机器学习卷积神经网络(CNN)

    卷积神经网络(CNN) 关注公众号"轻松学编程"了解更多. 一.简介 ​ 卷积神经网络(Convolutional Neural Network,CNN)是一种前馈神经网络,它的人 ...

  4. 卷积神经网络CNN总结

    从神经网络到卷积神经网络(CNN)我们知道神经网络的结构是这样的: 那卷积神经网络跟它是什么关系呢?其实卷积神经网络依旧是层级网络,只是层的功能和形式做了变化,可以说是传统神经网络的一个改进.比如下图 ...

  5. 【深度学习系列】手写数字识别卷积神经--卷积神经网络CNN原理详解(一)

    上篇文章我们给出了用paddlepaddle来做手写数字识别的示例,并对网络结构进行到了调整,提高了识别的精度.有的同学表示不是很理解原理,为什么传统的机器学习算法,简单的神经网络(如多层感知机)都可 ...

  6. 【深度学习系列】卷积神经网络CNN原理详解(一)——基本原理

    上篇文章我们给出了用paddlepaddle来做手写数字识别的示例,并对网络结构进行到了调整,提高了识别的精度.有的同学表示不是很理解原理,为什么传统的机器学习算法,简单的神经网络(如多层感知机)都可 ...

  7. 深度学习之卷积神经网络CNN及tensorflow代码实现示例

    深度学习之卷积神经网络CNN及tensorflow代码实现示例 2017年05月01日 13:28:21 cxmscb 阅读数 151413更多 分类专栏: 机器学习 深度学习 机器学习   版权声明 ...

  8. TensorFlow 2.0 深度学习实战 —— 浅谈卷积神经网络 CNN

    前言 上一章为大家介绍过深度学习的基础和多层感知机 MLP 的应用,本章开始将深入讲解卷积神经网络的实用场景.卷积神经网络 CNN(Convolutional Neural Networks,Conv ...

  9. 卷积神经网络(CNN)前向传播算法

    在卷积神经网络(CNN)模型结构中,我们对CNN的模型结构做了总结,这里我们就在CNN的模型基础上,看看CNN的前向传播算法是什么样子的.重点会和传统的DNN比较讨论. 1. 回顾CNN的结构 在上一 ...

  10. 卷积神经网络(CNN)反向传播算法

    在卷积神经网络(CNN)前向传播算法中,我们对CNN的前向传播算法做了总结,基于CNN前向传播算法的基础,我们下面就对CNN的反向传播算法做一个总结.在阅读本文前,建议先研究DNN的反向传播算法:深度 ...

随机推荐

  1. 同时配置github和gitee秘钥

    1.设置用户名和邮箱 git config --global --list 查看全局配置信息 git config --global --list 删除配置:必须删除该设置 git config -- ...

  2. P4747 [CERC2017] Intrinsic Interval 题解

    题目链接:Intrinsic Interval 讲讲析合树如何解决这种问题,其实这题很接近析合树的板题的应用. 增量法进行析合树建树时,需要用 ST 表预处理出 \(max\) 和 \(min\) 以 ...

  3. 创建大量栅格文件并分别写入像元数据:C++ GDAL代码实现

      本文介绍基于C++语言GDAL库,批量创建大量栅格遥感影像文件,并将数据批量写入其中的方法.   首先,我们来明确一下本文所需实现的需求.已知我们对大量遥感影像进行了批量读取与数据处理操作--具体 ...

  4. 高精度模板 大数加大数,可变数组vector实现

    vector<int> Add(vector<int>& A, vector<int>& B)//采用引用传入vector,避免将其全部复制传值,使 ...

  5. Excel如何核对同一行的两列数据是否一致

    方法一 Ctrl+G 快捷键Ctrl+G,点击[定位条件],选择"行内容差异单元格",点击[确定]. 方法二 条件格式 逆向思维,先利用条件格式查找出相同的数据,筛选剔除相同的数据 ...

  6. CF1878C Vasilije in Cacak 题解

    题目传送门 简化题意 有 \(t\) 组询问,每次询问是否能从 \(1 \sim n\) 中选择 \(k\) 个数使得它们的和为 \(x\). 解法 考虑临界情况,从 \(1 \sim n\) 中选择 ...

  7. 51单片机封装库HML_FwLib_STC89/STC11

    HML_FwLib_STC89/11 项目地址 https://github.com/MCU-ZHISHAN-IoT/HML_FwLib_STC89 https://github.com/MCU-ZH ...

  8. Javascript中的var变量声明作用域问题

    先看一下这两段代码的执行结果 var name2 = 'What!'; function a() { if (typeof name2 === 'undefined') { console.log(' ...

  9. golang中的接口(数据类型)

    golang中的接口 Golang 中的接口是一种抽象数据类型,Golang 中接口定义了对象的行为规范,只定义规范 不实现.接口中定义的规范由具体的对象来实现,通俗的讲接口就一个标准,它是对一个对象 ...

  10. python3 pip3 安装python-ldap失败

    pip3安装时提示 ERROR: Could not build wheels for python-ldap, uWSGI, M2Crypto, which is required to insta ...