目录
1 卷积神经网络(Convolutiona Neural Network, CNN)
摘要
本周学习了李宏毅深度学习关于卷积神经网络的课程。了解了什么是卷积神经网络,以及卷积神经网络相关概念的学习,包括卷积核、感受野、步长和填充参数共享、卷积层、池化等概念。还通过对LeNet-5网络的简单分析加深了对卷积神经网络整体流程的理解。最后基于PyTorch框架使用卷积神经网络简单实现了识别手写数字0-9,进一步加深了对卷积神经网络的理解。
Abstract
This week, I learned Li Hongyi's deep learning course on convolutional neural networks. This course has learned what a convolutional neural network is and the learning of related concepts of convolutional neural networks, including convolutional kernel, receptive field, step size and filling parameter sharing, convolutional layer, pooling, etc. A simple analysis of the LeNet-5 network also deepens the understanding of the overall process of convolutional neural networks. Finally, based on the PyTorch framework, the convolutional neural network is used to recognize the handwritten digits 0-9, which further deepens the understanding of the convolutional neural network.
1 卷积神经网络(Convolutiona Neural Network, CNN)
1.1 什么是卷积神经网络
卷积神经网络(Convolutional Neural Networks, CNN)是一类包含卷积计算且具有深度结构的前馈神经网络,是深度学习的代表算法之一。CNNA主要用于处理和分析具有网格结构的数据,特别是图像和视频数据。
在CNN图像分类中,图片就是三维的张量(3D tensor),代表长、宽、和RGB三通道(channel),再将三维张量拉直变为一个向量,由此可以作为神经网络的输入:
向量如果作为全连接神经网络的输入,由于向量长度很长,输出很大,隐藏层神经元也很多,就会增加计算的参数和量:
考虑影像本身的特性,其实不需要每个神经元和输入的每个维度进行全连接,对影像本身的特性进行观察有如下结论:
1.2 感受野(Receptive Field)
每个神经元只需要侦测图像中的局部特征图案(pattern),再将所有识别出来的特征图案综合起来判别图像类别。比如识别到鸟嘴、鸟爪、羽毛等特征,神经网络就会将图片识别为一只鸟。
则只需要侦测有没有特定的重要的图案出现,就可判断整体:
通过第一个观察,可以得到只需要让每个神经元都只关心与自己相关的某个区域里的情况,即感受野(Receptive field)里发生的事就可以了。
感受野的各个属性可以根据具体情况自行定义调整。
感受野最经典最常见的设置是 ,然后移动步长(stride),这个步长可以自行设定,但是一般都不会太大,超出做补丁(padding)。
感受野的大小不同可以影响卷积神经网络的性能和特征提取能力。较大的感受野可以捕捉到更全局的上下文信息,适用于涉及长程依赖的任务。而较小的感受野可以捕捉到局部的细节特征,适用于需要高精度的局部信息的任务。
1.3 参数共享(Parameter Sharing)
图案可能出现在图像的不同位置,但是总会被感受野里的神经元侦测到,这时就会存在一个问题,是否每个感受野都需要检测该图案的侦测器。
所以可以考虑让不同的感受野的神经元共享参数,虽然权重参数相同,但是输入不同,则不会造成结果相同的情况发生:
1.4 卷积层(Convolutional Layer)
换个角度来看CNN,假如存在一个个的滤波器(filter)也叫卷积核,对输入数据进行卷积操作。每个滤波器是一个小的二维矩阵或张量,用于从输入数据中提取特定的特征。通过在输入数据上滑动滤波器,进行局部特征的提取,生成输出特征图。
通过与卷积核进行内积运算,即输入中与卷积核形状相同的部分,分别与卷积核进行逐个元素相乘再相加。
下图为一张6x6的图像,卷积核是3x3,卷积核按步长为1进行卷积。
不同的滤波器扫过一张图片,将会产生“新的图片”,每个滤波器会产生图片中的一个特征图(feature map):
第一层的卷积结果产生了一张的特征图,继续卷积时,需要对这64个特征图都进行处理:
这里,在第二层中考虑 的范围,在原图实际上考虑了 范围内的图案。当卷积层越来越深时,即使只是 的滤波器,看到的范围也会越来越大:
卷积操作能够有效捕捉局部特征和空间结构,实现对输入数据的特征提取和表示。
构建全连接层:
- 将输入和输出变形为矩阵(宽度,高度)
- 将权重变形为4维张量(h,w)到(h',w')的全连接层:
- v是w的重新索引
对全连接层使用平移不变性和局部性得到卷积层:
- 为保证平移不变性,将v变成二维交叉相关 :
- 根据局部性原理,在评估 时,不应该用远离 的参数,则当 时,使 :
二维交叉相关和二维卷积层的区别
- 二维交叉相关,循环顺序是从左到右,从上到下:
- 二维卷积层,循环顺序是从右到左,从下到上:
- 由于对称性,在实际使用中二者没有区别
卷积层将输入和核矩阵进行交叉相关,再加上偏移后得到输出,而核矩阵和偏移都是可学习的参数。
关于二维互相关运算、二维卷积层、卷积核以及填充和步长的相关代码表示如下:
import torch
from torch import nn
import numpy as np
# 计算二维互相关运算
def corr2d(X, K):
h, w = K.shape
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j: j + w] * K).sum()
return Y
# 实现二维卷积层
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
return corr2d(x, self.weight) * self.bias
# 学习由X生成Y的卷积核
conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y)**2
conv2d.zero_grad()
l.sum().backward()
conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'batch {i+1}, loss {l.sum():.3f}')
# 所学的卷积核的权重张量
print(conv2d.weight.data.reshape((1, 2)))
# 填充和步长
def comp_conv2d(conv2d, X):
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
return Y.reshape(Y.shape[2:])
# 在所有侧边填充1个像素,除此之外可以设置填充不同的高度和宽度
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
print(comp_conv2d(conv2d, X).shape)
结合外参数(步长、填充)和内参数(卷积核),有以下规律:卷积核越大,输出越小;步幅越大,输出越小;填充越大,输出越大。
假设输入和卷积核均为方阵,设输入尺寸为 ,输出尺寸为, 卷积核尺寸为 ,填充为 ,步长为 ,则有以下关系:
如果输入和卷积核不为方阵,设输入尺寸为 ,输出尺寸为, 卷积核尺寸为 ,填充为 ,步长为 ,则输出尺寸 的计算公式为:
1.5 池化(Pooling)
为了减小特征图的尺寸以获取更高级别的特征,可以采用池化(Pooling)来解决。子采样(subsampling)把一张大的图片缩小,但并不会影响特征。
最大池化(Max Pooling):
选出每组最大的数保留
还有平均池化(Mean Pooling),是对邻域内特征点求平均值。
卷积和池化通常交替使用:
使用池化来做子采样可以把图像变小,减少运算量。
实现池化层的正向传播代码如下:
import torch
from torch import nn
import numpy as np
# 实现池化层的正向传播
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
# 最大池化
if mode == 'max':
Y[i, j] = X[i:i + p_h, j:j + p_w].max()
# 平均池化
elif mode == 'avg':
Y[i, j] = X[i:i + p_h, j:j + p_w].mean()
return Y
还能设定一个任意大小的矩形池化窗口,并分别设定填充和步幅的高度和宽度以及池化层在每个输入通道上单独运算。
1.6 CNN整体流程
CNN的基本流程为:
- 输入图像经过卷积层,由卷积核提取局部特征
- 经过池化层降采样,减少数据量并保留特征
- 多个卷积和池化层堆叠,提取越来越抽象的特征
- 最终经过全连接层,将特征映射到具体的分类标签或其他输出
如下图,以LeNet-5为例,输入的二维图像(单通道),先经过两次卷积层到池化层,再经过全连接层,最后为输出层:
整个 LeNet-5 网络总共包括7层(不含输入层),分别是:卷积层C1、池化层S2、卷积层C3、池化层S4、卷积层C5、全连接层F6、输出层OUTPUT。
尺寸关系计算如下:
- C1:输入尺寸W为 , 卷积核尺寸F为 ,填充 为2,步长 为1,则输出尺寸的计算为:
- S2: 输入尺寸W为 , 采样范围F为 ,填充 为0,步长 为2,则输出尺寸的计算为:
- 其他几层也是如此计算得到的。
则各层参数为:
- INPUT:输入是一个2维的图片,大小 ;
- C1:经过第一层卷积层,得到C1的6个 的特征映射图,6个说明了第一层卷积层用了6个卷积核。这里卷积后大小依旧为 ;
- S2:经过第一层池化层, 变成了 ,一般是每邻域四个像素中的最大值变为一个像素,相应图片的长和宽各缩小两倍。
- C3:经过第二层卷积层,变成了C3层的16个 的特征映射图
- S4:经过第二层池化层,得到S4层的16个 的特征映射图
- C5:经过第三层卷积层,得到C5层的120个 的特征映射图
- F6:这层为全连接层,得到F6层有84个节点
- OUTPUT:这层也为全连接层,共有10个节点,分别代表数字0到9
2 CNN实例——手写数字识别
2.1 数据集加载
手写数字识别使用的数据集MNIST是机器学习领域的标准数据集,其中的每一个样本都是一副二维的灰度图像,尺寸为28x28。输入就相当于一个单通道的图像,是二维的,在实现的时候要将每个样本图像转换为28*28的张量,作为输入。代码如下:
import torch
from torchvision import datasets, transforms
# 将图像转换为张量形式
data_transform = transforms.Compose([
transforms.ToTensor()
])
# 加载训练数据集
train_dataset = datasets.MNIST(
root='./', # 下载路径
train=True, # 是训练集
transform=data_transform # 数据集转换参数
)
# 批次加载器
train_dataloader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=16, shuffle=True)
# 加载测试数据集
test_dataset = datasets.MNIST(
root='./', # 下载路径
train=False, # 是训练集
transform=data_transform # 数据集转换参数
)
# 批次加载器
test_dataloader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=16, shuffle=True)
2.2 LeNet-5 网络
这次搭建的为LeNet-5卷积神经网络,网络流程结构在上一节中已经分析过了,代码如下,其中in_channels为输入通道数、out_channels为输出通道数、kernel_size:为卷积核尺寸、padding为填充,不写则默认0、stride为步长,不写则默认1:
import torch
from torch import nn
# 定义网络模型
class MyLeNet5(nn.Module):
# 初始化网络
def __init__(self):
super(MyLeNet5, self).__init__()
self.net = nn.Sequential(
nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5), nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2),
nn.Conv2d(in_channels=16, out_channels=120, kernel_size=5), nn.ReLU(),
nn.Flatten(),
nn.Linear(120, 84), nn.Tanh(),
nn.Linear(84, 10)
)
# 前向传播
def forward(self, x):
y = self.net(x)
return y
2.3 模型训练
网络搭建好之后,所有的内参数(即卷积核)都是随机的,所有要通过训练尽可能提高网络的预测能力。在训练前,首先要选择损失函数(这里使用交叉熵损失函数)以及定义优化器、进行学习率的调整。然后写一个用于训练网络的函数,四个参数分别是批次加载器、模型、损失函数、优化器,代码如下:
import torch
from torch import nn
from net import MyLeNet5
from torch.optim import lr_scheduler
# 调用net,将模型数据转移到gpu
model = MyLeNet5().to(device)
# 选择损失函数
loss_fn = nn.CrossEntropyLoss() # 交叉熵损失函数,自带Softmax激活函数
# 定义优化器
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3, momentum=0.9)
# 学习率每隔10轮次, 变为原来的0.1
lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)
# 定义模型训练函数
def train(dataloader, model, loss_fn, optimizer):
loss, current, n = 0.0, 0.0, 0
for batch, (X, y) in enumerate(dataloader):
# 前向传播
X, y = X.to(device), y.to(device)
output = model(X)
cur_loss = loss_fn(output, y)
_, pred = torch.max(output, dim=1)
# 计算当前轮次时,训练集的精确度
cur_acc = torch.sum(y == pred) / output.shape[0]
# 反向传播
optimizer.zero_grad()
cur_loss.backward()
optimizer.step()
loss += cur_loss.item()
current += cur_acc.item()
n = n + 1
print("train_loss: ", str(loss / n))
print("train_acc: ", str(current / n))
2.3 模型测试
这里和模型训练类似,只不过要观察训练好的模型,在测试集的预测效果。与训练的代码相似,只是没有了反向传播优化参数的过程。将测试集的精确度作为返回值,在外围调用这个函数时,可以通过循环找到测试集最大的精确度。用于测试的函数代码如下:
# 定义模型测试函数
def test(dataloader, model, loss_fn):
model.eval()
loss, current, n = 0.0, 0.0, 0
# 该局部关闭梯度计算功能,提高运算效率
with torch.no_grad():
for batch, (X, y) in enumerate(dataloader):
# 前向传播
X, y = X.to(device), y.to(device)
output = model(X)
cur_loss = loss_fn(output, y)
_, pred = torch.max(output, dim=1)
# 计算当前轮次时,训练集的精确度
cur_acc = torch.sum(y == pred) / output.shape[0]
loss += cur_loss.item()
current += cur_acc.item()
n = n + 1
print("test_loss: ", str(loss / n))
print("test_acc: ", str(current / n))
return current / n # 返回精确度
2.4 循环训练与测试
设定一个训练轮次epochs,此处epochs=50,每经过一个epoch的训练,就进行测试,实时打印观察训练集和测试集的拟合情况。当测试集的精确度是当前的最大值时,就保存这个模型的参数到save_model/best_model.pth,代码如下:
import os
# 开始训练
epoch = 50
max_acc = 0
for t in range(epoch):
print("="*33)
print(f"epoch {t + 1}")
print("-" * 33)
train(train_dataloader, model, loss_fn, optimizer)
a = test(test_dataloader, model, loss_fn)
# 保存最好的模型参数
if a > max_acc:
folder = 'save_model'
if not os.path.exists(folder):
os.mkdir(folder)
max_acc = a
print("current best model acc = ", a)
torch.save(model.state_dict(), 'save_model/best_model.pth')
print("=" * 33)
print("Done!")
2.5 训练结果
运行之后可以发现,测试集的精度经过1个epochs就达到了95%左右:
最终经过50轮次的训练,测试集精度达到了99%左右:
模型参数也得以保存:
总结
通过本周学习,了解了CNN是一种深度学习模型,广泛应用于图像识别、目标检测、自然语言处理等领域。CNN通常由多层卷积、池化和最后的全连接层组成。卷积层主要负责提取输入数据的特征。池化层用于对卷积层输出的特征图进行降维和抽样,以减少模型参数数量和计算复杂度。全连接层通常位于卷积神经网络的最后几层,负责将卷积层和池化层提取的特征进行组合和分类。除此之外,在进行卷积、池化时,卷积核大小、步长和填充的尺寸都是十分重要的。
最后通过识别手写数字0-9这个实例,加深了CNN的理解,也加深了对PyTorch各个主要模块功能的认识,简单学习理解了CNN在具体实践中的应用。
标签:loss,nn,10.28,torch,11.3,卷积,model,size,周报 From: https://blog.csdn.net/qq_60040306/article/details/143280386