基于PyTorch对卷积神经网络原理总结与实现

1.概述

1.1 卷积神经网络的引入

       在前一个博客中我们对于全连接神经网络做了一个实现。其中输入层有784个(28*28的图片)元素,四个隐藏层分别有400、300、200、100个神经元,输出层包括10个手写数字的类别,要表达这样一个很小的神经网络需要的权重约为:520200(784*400+300*200+200*100+100*10)个,如果每个权重使用4个字节的浮点数表示,这些权重会占用2080800字节,约为1.98MB,超出了当时计算机的内存大小。

     于是,人们开始考虑能否将全连接神经网络的连接方式加以改变,这是前馈神经网络应运而生,卷积神经网络就是一个包含卷积运算且具有深度结构的前馈神经网络。卷积神经网络成功的原因在于其采用的局部连接和权值共享的方式,一方面减少了权值的数量,使得网络易于优化,另一方面降低了模型的复杂度,也就是减少了过拟合的风险,卷积神经网络在大型图像处理中有出色的表现。

1.2 卷积神经网络的基本准则

1.2.1局部性

       指通过检测图片中的局部特征来决定图片的类别

1.2.2相同性

        指检测不同的图片是否具有相同的特征,虽然这些特征可能会出现在不同的地方,但是仍然可以通过局部特征来进行判断。

1.2.3不变性

        指对一张图片进行下采样时(对于一个样值序列,间隔几个样值取样一次,这样得到的新序列就是原序列的下采样),图片的性质基本保持不变

       基于以上三个准则,典型的卷积神经网络至少有四个部分构成:输入层、卷积层、池化层以及全连接层。卷积层负责提取图片的局部特征;池化层用于大幅度降低参数的数量级(降维);全连接层类似传统的神经网络没用来输出预测的结果。

2.网络层次分析

2.1 卷积层

      给定一个图像,CNN并不能准确的知道这些特征要匹配原图的哪些部分,所以会在原图中的每个可能的位置都进行尝试,相当于把这些特征当做一个滤波器(也称为卷积核)。这个用来匹配的过程就称为卷积。与全连接神经网络不同的是,卷积神经网络中每个神经元只与输入数据的一个局部区域连接,因为滤波器提取到的是局部特征。与神经元连接的空间大小(即感受视野的大小,即滤波器的宽度和高度)是需要人工设置的。

2.1.1滤波器的高度和宽度

      每个滤波器的工作就是在输入数据中寻找一种特征,每个滤波器的宽度和高度都比较小,但是深度和输入数据的深度保持一致。

2.1.2 步长

       步长表示每个滤波器每次移动的距离

2.1.3 边界填充

       卷积运算会使卷积图片的大小不断的变小,且由于图片左上角的元素只被一个输出使用,所以在图片边缘的像素,在输出中会被较少的使用,也就意味着很多的边缘信息会被丢失。为了解决这两个问题引入了边界填充(padding操作),也就是在图片卷积操作之前,沿着图片边缘用0进行边界填充。当步长等于1时,使用0填充能够使输入和输出的数据具有相同的空间尺寸。

       设输入的图片尺寸为w_{i}\times h_{i}\times d_{i},步长为s,边界填充的大小为p,滤波器的尺寸为f\times f,则输出图片的尺寸为w_{o}\times h_{o}\times d_{o} 其计算公式如下所示:

w_{o}=\frac{w_{i}-f+2\times p}{s}+1

h_{o}=\frac{h_{i}-f+2\times p}{s}+1

d_{o}=d_{i}

      当输入图片的尺寸为5*5*3,滤波器尺寸为3*3,步长为1,边界填充的大小为0时,可以计算出输出图片的大小尺寸为3*3*3。

      卷积就是做滤波器和输入图片的矩阵内积操作,单个通道的卷积过程如下图所示:

        在卷积层使用参数共享可以有效地减少参数地个数,参数之所以能够共享是因为特征地相同性,即一个特征在不同位置地表现是相同的。参数共享包括共享滤波器和共享权重向量等。卷积在PyTorth中通常采用torch.nn.Conv2d()函数实现,首先需要输入一个torch.autograd.Variable类型的变量,其大小是(batch, channel,H,W),其中batch表示输入图片的数量,channel表示输入图片的通道数,H和W表示输入图片的高度和宽度,例如输入32张彩色图片,图片的高度和宽度分别是50,100那么输入变量的大小就是(32, 3, 50, 100)

2.1.4 卷积层代码实现

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import torch
from torch import nn
from torch.autograd import Variable

im = Image.open(r'E:\kaggleData\dog\images\Images\n02085620-Chihuahua\n02085620_199.jpg').convert('L') # 转为灰度图像
im = np.array(im, dtype='float32') # 将其转化为一个矩阵
plt.imshow(im.astype('uint'), cmap='gray')
plt.show()

# 将图片矩阵转化为PyTorch中的tensor,并适配卷积输入的要求
"""
    卷积在PyTorch中通常采用的是torch.nn.Conv2d()函数实现。首先需要输入一个torch.autograd.Variable类型的变量
其大小是(batch,channels, H, W)其中batch表示输入图片的数量,channels表示输入图片的通道数
一般彩色的图片通道数为3,灰度图片的通道数为1,而在卷积的过程中通道数会比较大,会出现几十到几百个通道。
"""
im = torch.from_numpy(im.reshape((1, 1, im.shape[0], im.shape[1])))
# 使用Conv2d()函数
conv1 = nn.Conv2d(1, 1, 3, bias=False)
# 定义轮廓检验算子
sobel_kernel = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], dtype='float32')
# 适配卷积的输入/输出
sobel_kernel = sobel_kernel.reshape((1, 1, 3, 3))
# 给卷积的卷积核赋值
conv1.weight.data = torch.from_numpy(sobel_kernel)
# 作用在图片上
edge1 = conv1(Variable(im))
# 将输出转化为图片的格式
edge1 = edge1.data.squeeze().numpy()
plt.imshow(edge1, cmap='gray')
plt.show()

2.2 池化层

2.2.1池化层概述

       池化层和卷积层一样,也是针对局部区域进行处理,池化层利用一个空间窗口(滤波器),通常取这些空间窗口中的最大值/加权平均值作为输出结果。然后不断滑动窗口,对输入图片的每个卷积操作结果进行单独处理,减小其尺寸空间。池化层的作用如下:

      1.特征降维,避免过拟合

      经过卷积操作的图片会含有非常多的特征,所以需要通过池化层对特征进行降维处理,池化处理也称为下采样。

      2.空间不变性

       池化层能在图片空间变化(旋转、压缩、平移)时而保持其特征不变。例如一张小狗的照片,像素很多时小狗很清晰,对图片进行压缩后,小狗变小了,但是仍然可以看出图片中是一个小狗,且其主要的特征没有变化。

      3.减少参数,降低训练难度

     池化处理一般分为最大池化和平均池化两种,最大池化就是在池化空间窗口取最大值,平均池化就是在池化空间窗口取加权平均值。

      在PyTorch中常用nn.MaxPool2d()函数实现最大池化处理,该函数对于输入图片的要求与torch.nn.Conv2d()函数相同

2.2.2池化层实现

from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
import torch
from torch import nn
from torch.autograd import Variable


# 读取一张彩色图片转化为灰度图片
im = Image.open(r'E:\kaggleData\computer_version_Data\test.jpg').convert('L')
# 转为矩阵
im = np.array(im, dtype='float32')
"""
    Python中与数据类型相关函数及属性有如下三个:type/dtype/astype。
    type() 返回参数的数据类型    
    dtype 返回数组中元素的数据类型    
    astype() 对数据类型进行转换
"""
plt.imshow(im.astype('uint8'), cmap='gray')
plt.show()

# 将图片矩阵转化为tensor,并适配卷积输入的要求
im = torch.from_numpy(im.reshape((1, 1, im.shape[0], im.shape[1])))
pool1 = nn.MaxPool2d(2, 2)
print('before max pool, image shape:{} x {}'.format(im.shape[2], im.shape[3]))

small_im1 = pool1(Variable(im))
# 将输出转化为图片的格式
small_im1 = small_im1.data.squeeze().numpy()
print('after max poolm, image shape:{} x {}'.format(small_im1.shape[0], small_im1.shape[1]))
plt.imshow(small_im1, cmap='gray')
plt.show()

      左图是在最大池化之前,右图是最大池化之后的结果。

       可以看到经过经过池化后的图像仍然很清晰。

3.基于PyTorch的卷积神经网络对手写数字进行识别

          一共定义了五层,其中两层卷积层,两层池化层,最后一层为FC层进行分类输出。其网络结构如下:

# 包
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.data as Data

# torchvision 包收录了若干重要的公开数据集、网络模型和计算机视觉中的常用图像变换
import torchvision
import torchvision.transforms as transforms

import matplotlib.pyplot as plt
import numpy as np

# 设备配置
#torch.cuda.set_device(1) # 这句用来设置pytorch在哪块GPU上运行
#device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 超参数设置
num_epochs = 5
num_classes = 10
batch_size = 64  # 一个batch 的大小
image_size = 28  #图像的总尺寸28*28
learning_rate = 0.001

# transform=transforms.ToTensor():将图像转化为Tensor,在加载数据的时候,就可以对图像做预处理
train_dataset = torchvision.datasets.MNIST(root='./data',train=True,transform=transforms.ToTensor(),download=True)
test_dataset = torchvision.datasets.MNIST(root='./data',train=False,transform=transforms.ToTensor(),download=True)

# 训练数据集的加载器,自动将数据分割成batch,顺序随机打乱
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,batch_size=batch_size,shuffle=True)
print('len(train_loader):',len(train_loader))
print('len(train_loader.dataset):',len(train_loader.dataset))

"""
接下来把测试数据中的前5000个样本作为验证集,后5000个样本作为测试集
"""
indices = range(len(test_dataset))
indices_val = indices[:5000]
indices_test = indices[5000:]

# 通过下标对验证集和测试集进行采样
sampler_val = torch.utils.data.sampler.SubsetRandomSampler(indices_val)
sampler_test = torch.utils.data.sampler.SubsetRandomSampler(indices_test)

# 根据采样器来定义加载器,然后加载数据
validation_loader = torch.utils.data.DataLoader(dataset =test_dataset,batch_size = batch_size,sampler = sampler_val)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset,batch_size=batch_size,sampler = sampler_test)

#从数据集中读入一张图片,并绘制出来
idx = 0
#dataset支持下标索引,其中提取出来的每一个元素为features,target格式,即属性和标签。[0]表示索引features
muteimg = train_dataset[idx][0].numpy()
#由于一般的图像包含rgb三个通道,而MINST数据集的图像都是灰度的,只有一个通道。因此,我们忽略通道,把图像看作一个灰度矩阵。

#用imshow画图,会将灰度矩阵自动展现为彩色,不同灰度对应不同颜色:从黄到紫

plt.imshow(muteimg[0,...])
print('标签是:',train_dataset[idx][1])

# 定义两个卷积层的厚度(feature map的数量)
depth = [4, 8]


class ConvNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, depth[0], 5,
                               padding=2)  # 1 input channel, 4 output channels, 5x5 square convolution kernel
        self.pool = nn.MaxPool2d(2, 2)  # 定义一个Pooling层
        self.conv2 = nn.Conv2d(depth[0], depth[1], 5,
                               padding=2)  # 第二层卷积:4input channel, 8 output channels, 5x5 square convolution kernel
        self.fc1 = nn.Linear(depth[1] * image_size // 4 * image_size // 4, 512)  # 线性连接层的输入尺寸为最后一层立方体的平铺,输出层512个节点
        self.fc2 = nn.Linear(512, num_classes)  # 最后一层线性分类单元,输入为512,输出为要做分类的类别数

    def forward(self, x):
        # x尺寸:(batch_size, image_channels, image_width, image_height)
        x = F.relu(self.conv1(x))  # 第一层卷积的激活函数用ReLu
        x = self.pool(x)  # 第二层pooling,将片变小

        # x的尺寸:(batch_size, depth[0], image_width/2, image_height/2)
        x = F.relu(self.conv2(x))  # 第三层卷积,输入输出通道分别为depth[0]=4, depth[1]=8
        x = self.pool(x)  # 第四层pooling,将图片缩小到原大小的1/4

        # x的尺寸:(batch_size, depth[1], image_width/4, image_height/4)
        # view函数将张量x变形成一维的向量形式,总特征数batch_size * (image_size//4)^2*depth[1]不改变,为接下来的全连接作准备。
        x = x.view(-1, image_size // 4 * image_size // 4 * depth[1])

        # x的尺寸:(batch_size, depth[1]*image_width/4*image_height/4)
        x = F.relu(self.fc1(x))  # 第五层为全链接,ReLu激活函数

        # x的尺寸:(batch_size, 512)
        # dropout 参数training:pply dropout if is True. Defualt: True
        x = F.dropout(x, training=self.training)  # 以默认为0.5的概率对这一层进行dropout操作,为了防止过拟合
        x = self.fc2(x)

        # x的尺寸:(batch_size, num_classes)
        # 输出层为log_softmax,即概率对数值log(p(x))。采用log_softmax可以使得后面的交叉熵计算更快
        # log_softmax虽然等价于log(softmax(x)),但是分开两个运算会速度比较慢,数值也不稳定。
        # dim=0 ,即softmax后横向的和为1
        x = F.log_softmax(x, dim=0)
        return x

    def retrieve_features(self, x):
        # 该函数专门用于提取卷积神经网络的特征图的功能,返回feature_map1, feature_map2为前两层卷积层的特征图
        feature_map1 = F.relu(self.conv1(x))  # 完成第一层卷积
        x = self.pool(feature_map1)  # 完成第一层pooling
        # print('type(feature_map1)=',feature_map1)
        # type是一个四维的tensor
        feature_map2 = F.relu(self.conv2(x))  # 第二层卷积,两层特征图都存储到了feature_map1, feature_map2中
        return (feature_map1, feature_map2)

"""计算预测正确率的函数,其中predictions是模型给出的一组预测结果:batch_size行num_classes列的矩阵,labels是真正的label"""
def accuracy(predictions, labels):
    # torch.max的输出:out (tuple, optional维度) – the result tuple of two output tensors (max, max_indices)
    pred = torch.max(predictions.data, 1)[1] # 对于任意一行(一个样本)的输出值的第1个维度,求最大,得到每一行的最大元素的下标
    right_num = pred.eq(labels.data.view_as(pred)).sum() #将下标与labels中包含的类别进行比较,并累计得到比较正确的数量
    return right_num, len(labels) #返回正确的数量和这一次一共比较了多少元素


net = ConvNet()

criterion = nn.CrossEntropyLoss()  # Loss函数的定义,交叉熵
optimizer = torch.optim.SGD(net.parameters(), lr=0.001, momentum=0.9)  # 定义优化器,普通的随机梯度下降算法

record = []  # 记录训练集和验证集上错误率的list
weights = []  # 每若干步就记录一次卷积核

for epoch in range(num_epochs):
    train_accuracy = []  # 记录训练数据集准确率的容器

    # 一次迭代一个batch的 data 和 target
    for batch_id, (data, target) in enumerate(train_loader):
        net.train()  # 给网络模型做标记,打开关闭net的training标志,从而决定是否运行dropout

        output = net(data)  # forward
        loss = criterion(output, target)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        accuracies = accuracy(output, target)
        train_accuracy.append(accuracies)

        if batch_id % 100 == 0:  # 每间隔100个batch执行一次打印等操作
            net.eval()  # 给网络模型做标记,将模型转换为测试模式。
            val_accuracy = []  # 记录校验数据集准确率的容器

            for (data, target) in validation_loader:  # 计算校验集上面的准确度
                output = net(data)  # 完成一次前馈计算过程,得到目前训练得到的模型net在校验数据集上的表现
                accuracies = accuracy(output, target)  # 计算准确率所需数值,返回正确的数值为(正确样例数,总样本数)
                val_accuracy.append(accuracies)

            # 分别计算在已经计算过的训练集,以及全部校验集上模型的分类准确率

            # train_r为一个二元组,分别记录目前  已经经历过的所有  训练集中分类正确的数量和该集合中总的样本数,
            train_r = (sum([tup[0] for tup in train_accuracy]), sum([tup[1] for tup in train_accuracy]))
            # val_r为一个二元组,分别记录校验集中分类正确的数量和该集合中总的样本数
            val_r = (sum([tup[0] for tup in val_accuracy]), sum([tup[1] for tup in val_accuracy]))

            # 打印准确率等数值,其中正确率为本训练周期Epoch开始后到目前batch的正确率的平均值
            print('Epoch [{}/{}] [{}/{}]\tLoss: {:.6f}\t训练集准确率: {:.2f}%\t验证集准确率: {:.2f}%'.format(
                epoch + 1, num_epochs, batch_id * batch_size, len(train_loader.dataset),
                loss.item(),
                100. * train_r[0] / train_r[1],
                100. * val_r[0] / val_r[1]))

            # 将准确率和权重等数值加载到容器中,方便后续处理

            record.append((100 - 100. * train_r[0] / train_r[1], 100 - 100. * val_r[0] / val_r[1]))

            # weights记录了训练周期中所有卷积核的演化过程。net.conv1.weight就提取出了第一层卷积核的权重
            # clone的意思就是将weight.data中的数据做一个拷贝放到列表中,否则当weight.data变化的时候,列表中的每一项数值也会联动
            '''这里使用clone这个函数很重要'''
            weights.append([net.conv1.weight.data.clone(), net.conv1.bias.data.clone(),
                            net.conv2.weight.data.clone(), net.conv2.bias.data.clone()])

识别结果:

热门文章

暂无图片
编程学习 ·

exe4j详细使用教程(附下载安装链接)

一、exe4j介绍 ​ exe4j是一个帮助你集成Java应用程序到Windows操作环境的java可执行文件生成工具,无论这些应用是用于服务器,还是图形用户界面(GUI)或命令行的应用程序。如果你想在任务管理器中及Windows XP分组的用户友好任务栏…
暂无图片
编程学习 ·

AUTOSAR从入门到精通100讲(126)-浅谈车载充电系统通信方案

01 引言 本文深入研究车载充电系统策略,设计出一套基于电动汽车电池管理系统与车载充电机的CAN通信协议,可供电动汽车设计人员参考借鉴。 02 电动汽车充电系统通讯网络 电动汽车整车控制系统中采用的是CAN总线通信方式,由一个整车内部高速CAN网络、内部低速CAN网络和一个充电…
暂无图片
编程学习 ·

CMake(九):生成器表达式

当运行CMake时,开发人员倾向于认为它是一个简单的步骤,需要读取项目的CMakeLists.txt文件,并生成相关的特定于生成器的项目文件集(例如Visual Studio解决方案和项目文件,Xcode项目,Unix Makefiles或Ninja输入文件)。然…
暂无图片
编程学习 ·

47.第十章 网络协议和管理配置 -- 网络配置(八)

4.3.3 route 命令 路由表管理命令 路由表主要构成: Destination: 目标网络ID,表示可以到达的目标网络ID,0.0.0.0/0 表示所有未知网络,又称为默认路由,优先级最低Genmask:目标网络对应的netmaskIface: 到达对应网络,应该从当前主机哪个网卡发送出来Gateway: 到达非直连的网络,…
暂无图片
编程学习 ·

元宇宙技术基础

请看图: 1、通过AR、VR等交互技术提升游戏的沉浸感 回顾游戏的发展历程,沉浸感的提升一直是技术突破的主要方向。从《愤怒的小鸟》到CSGO,游戏建模方式从2D到3D的提升使游戏中的物体呈现立体感。玩家在游戏中可以只有切换视角,进而提升沉浸…
暂无图片
编程学习 ·

flink的伪分布式搭建

一 flink的伪分布式搭建 1.1 执行架构图 1.Flink程序需要提交给 Job Client2.Job Client将作业提交给 Job Manager3.Job Manager负责协调资源分配和作业执行。 资源分配完成后,任务将提交给相应的 Task Manage。4.Task Manager启动一个线程以开始执行。Task Manage…
暂无图片
编程学习 ·

十进制正整数与二进制字符串的转换(C++)

Function one: //十进制数字转成二进制字符串 string Binary(int x) {string s "";while(x){if(x % 2 0) s 0 s;else s 1 s;x / 2;}return s; } Function two: //二进制字符串变为十进制数字 int Decimal(string s) {int num 0, …
暂无图片
编程学习 ·

[含lw+源码等]微信小程序校园辩论管理平台+后台管理系统[包运行成功]Java毕业设计计算机毕设

项目功能简介: 《微信小程序校园辩论管理平台后台管理系统》该项目含有源码、论文等资料、配套开发软件、软件安装教程、项目发布教程等 本系统包含微信小程序做的辩论管理前台和Java做的后台管理系统: 微信小程序——辩论管理前台涉及技术:WXML 和 WXS…
暂无图片
编程学习 ·

树莓派驱动DHT11温湿度传感器

1,直接使用python库 代码如下 import RPi.GPIO as GPIO import dht11 import time import datetimeGPIO.setwarnings(True) GPIO.setmode(GPIO.BCM)instance dht11.DHT11(pin14)try:while True:result instance.read()if result.is_valid():print(ok)print(&quo…
暂无图片
编程学习 ·

ELK简介

ELK简介 ELK是三个开源软件的缩写,Elasticsearch、Logstash、Kibana。它们都是开源软件。不过现在还新增了一个 Beats,它是一个轻量级的日志收集处理工具(Agent),Beats 占用资源少,适合于在各个服务器上搜集日志后传输给 Logstas…
暂无图片
编程学习 ·

Linux 基础

通常大数据框架都部署在 Linux 服务器上,所以需要具备一定的 Linux 知识。Linux 书籍当中比较著名的是 《鸟哥私房菜》系列,这个系列很全面也很经典。但如果你希望能够快速地入门,这里推荐《Linux 就该这么学》,其网站上有免费的电…
暂无图片
编程学习 ·

Windows2022 无线网卡装不上驱动

想来 Windows2022 和 windows10/11 的驱动应该差不多通用的,但是死活装不上呢? 搜一下,有人提到 “默认安装时‘无线LAN服务’是关闭的,如果需要开启,只需要在“添加角色和功能”中,选择开启“无线LAN服务…
暂无图片
编程学习 ·

【嵌入式面试宝典】版本控制工具Git常用命令总结

目录 创建仓库 查看信息 版本回退 版本检出 远程库 Git 创建仓库 git initgit add <file> 可反复多次使用&#xff0c;添加多个文件git commit -m <message> 查看信息 git status 仓库当前的状态git diff 差异对比git log 历史记录&#xff0c;提交日志--pret…
暂无图片
编程学习 ·

用Postman生成测试报告

newman newman是一款基于nodejs开发的可以运行postman脚本的工具&#xff0c;使用Newman&#xff0c;可以直接从命令运行和测试postman集合。 安装nodejs 下载地址&#xff1a;https://nodejs.org/en/download/ 选择自己系统相对应的版本内容进行下载&#xff0c;然后傻瓜式安…
暂无图片
编程学习 ·

Java面向对象之多态、向上转型和向下转型

文章目录前言一、多态二、引用类型之间的转换Ⅰ.向上转型Ⅱ.向下转型总结前言 今天继续Java面向对象的学习&#xff0c;学习面向对象的第三大特征&#xff1a;多态&#xff0c;了解多态的意义&#xff0c;以及两种引用类型之间的转换&#xff1a;向上转型、向下转型。  希望能…