建议先阅读我之前的深度学习博客,掌握一定的深度学习前置知识后再阅读本文,链接如下:
带你从入门到精通——深度学习(一. 深度学习简介和PyTorch入门)-CSDN博客
带你从入门到精通——深度学习(二. PyTorch中的类型转换、运算和索引)-CSDN博客
带你从入门到精通——深度学习(三. PyTorch中张量的形状重塑、拼接和自动微分)-CSDN博客
带你从入门到精通——深度学习(四. 神经网络的概念、激活函数和参数初始化)-CSDN博客
带你从入门到精通——深度学习(五. 神经网络的搭建、损失函数和反向传播)-CSDN博客
带你从入门到精通——深度学习(六. 神经网络的优化方法和正则化方法)-CSDN博客
带你从入门到精通——深度学习(七. 深度学习项目构建流程)-CSDN博客
目录
八. 卷积神经网络
8.1 图像的基本概念
在计算机中,图像是由各个像素点组成的,各个像素点的取值范围为[0, 255] 。
在灰度颜色空间中,图片只有一个通道(即黑白图),每个像素的值表示该点的亮度,像素值越接近于0,越接近于黑色;像素值越接近于255,越接近于白色。
在RGB颜色空间中,图片有三个通道(即彩色图),在RGB颜色空间中,使用红、绿、蓝三种基本颜色的不同强度组合来表示各种颜色,不同通道中的像素值越大,则该通道所对应的原色的强度越大,深度学习中使用的图像大多是RGB颜色空间中的彩色图,如下图所示:
RGBA颜色空间是RGB颜色空间的扩展,它在RGB颜色空间的基础上增加了一个额外的透明度通道(Alpha通道),Alpha通道中的像素值越大,则不透明度越大,像素值为0则表示完全透明,像素值为255则表示完全不透明。
在HSV颜色空间中,图片也有三个通道,H(hue,色调)通道表示颜色的类型,S(saturation,饱和度)通道表示颜色的纯度,V(value,明度)通道表示颜色的亮度。
在matplotlib中,使用img = plt.imread(img_path)可以读取指定路径下的图像数据,使用plt.imshow(img)可以绘制图像数据的对应图像。
8.2 卷积神经网络概述
卷积神经网络(Convolutional Neural Network,CNN)是指含有卷积层的神经网络,该网络主要由以下三部分构成:
卷积层:负责提取图像中的局部空间特征。
池化层:负责降维,减少参数量,提高运算速度。
全连接层:复杂输出最终的结果。
8.3 卷积层
8.3.1 卷积的计算流程
假设有如下的输入图像(input)、卷积核(也叫滤波器,filter):
卷积最终的输出output,被称为特征图(Feature Map)。
在第一次卷积计算中,卷积核与图片的左上角对应位置进行逐元素相乘,即计算点乘,最后再将各个元素相加得到最后的计算结果并放在特征图左上角,图示如下:
每次计算之后,卷积核会从左到右、从上到下进行移动,最后一次卷积计算即为图片右下角与卷积核的逐元素相乘并相加,计算得到的结果放在特征图右下角,图示如下:
8.3.2 padding和stride
卷积计算后得到的特征图尺寸通常比原始图像的尺寸更小,可以在原始图像周围进行填充(padding)操作,可以增大卷积计算后得到的特征图尺寸,图示如下:
Stride表示卷积核每次的移动步长,在步长为1的情况下移动卷积核,移动过程如下图:
而如果把stride增大为2,卷积核的移动过程如下图:
8.3.3 多通道卷积计算
RGB颜色空间中的图像以及HSV颜色空间中的图像都是由多个通道组成的,对于多通道卷积,其计算方法如下:
8.3.4 多卷积核卷积计算
当使用多个卷积核进行计算时,最终也会得到多张特征图,其计算方法如下:
8.3.5 特征图尺寸计算方法
在卷积层中,最终输出特征图尺寸除了与输入图像尺寸相关以外还与以下参数相关:
Size:卷积核的尺寸,一般会选择为奇数,例如1*1卷积核、3*3卷积核、5*5卷积核。
Padding:填充的尺寸,padding为1则表示在原图周围填充一圈像素点,padding为2则表示在原图周围填充两圈像素点。
Stride:卷积核的移动步长。
输出特征图尺寸计算方法如下图所示:
在上述公式中,W表示输入图像的尺寸、F表示卷积核大小、S表示步长、P表示填充尺寸、N表示输出特征图的尺寸(默认向下取整)。
8.3.6 卷积层的API和反向传播
在PyTorch中,卷积层的API为torch.nn.Conv2d(in_channels,out_channels,kernel_size, stride,padding) ,其中in_channels表示输入数据的通道数, out_channels:表示输出数据的通道数,也可以理解为卷积核(kernel)的数量,kernel_size:表示卷积核的尺寸,stride:卷积核移动的步长,padding:填充的尺寸,默认在输入数据周围补0。
卷积层的反向传播过程可以类比全连接层的反向传播过程,可以将卷积核视为一组共享参数的神经元,而图像数据与之部分连接。将图片的每个像素点从左到右、从上往下视为x1、x2、...、xn,一共n个输入,卷积核内部也从左到右、从上往下视为w1、w2、...、wm,一共m个权重,经过卷积运算后得到的输出特征图中的像素点也从左到右、从上往下视为y1、y2、...、yk,一共k个输出,由此可以写出经过卷积运算后y与x的函数表达式,例如:输入图片的尺寸为4 * 4,卷积核的尺寸为2 * 2,padding为0,stride为1,由上述的编码方法可得:y1 = x1 * w1 + x2 * w2 + x5 * w5 + x6 * w6,其余的y值以此类推,此时y与x的函数表达式就类似于经过全连接层后y与x的函数表达式,之后便可使用BP算法进行卷积层的反向传播并更新卷积核的权重参数。
8.4 池化层
8.4.1 池化的计算流程
池化(pooling)的计算流程以及池化层的padding和stride和卷积层基本一致,区别为卷积核的输出特征图中每个元素的计算方法为输入图像与卷积核对应位置的逐元素相乘并相加,而池化层的输出特征图中每个元素的计算方法为求输入图像与池化窗口对应位置的最大值或平均值,具体示例如下:
8.4.2 多通道池化计算
与多通道卷积计算不同,多通道池化计算不会将原图的通道数置为1,而是保持输出特征图与原图的通道数相等,其计算方法如下:
注意:池化层没有多池化窗口池化计算。
8.4.3 池化层的API和反向传播
在PyTorch中,最大池化层的API为torch.nn.MaxPool2d(kernel_size, stride,padding) ,平均池化层的API为torch.nn.AvgPool2d(kernel_size, stride,padding) ,其中kernel_size:表示池化窗口的尺寸,stride:池化窗口移动的步长,padding:填充的尺寸,默认在输入数据周围补0。
池化层的反向传播过程可以类比激活函数的反向传播过程,可以将池化层视为一个激活函数。将图片的每个像素点从左到右、从上往下视为x1、x2、...、xn,一共n个输入,经过池化窗口后得到的输出特征图中的像素点也从左到右、从上往下视为y1、y2、...、yk,一共k个输出,由此可以写出经过池化窗口后y与x的函数表达式,以最大池化为例:输入图片的尺寸为4 * 4,池化窗口的尺寸为2 * 2,padding为0,stride为1,由上述的编码方法可得:y1 = max(x1,x2,x5,x6),其余的y值以此类推,此时y与x的函数表达式就类似于经过一个激活函数(这个激活函数就是一个max函数)后的y与x的函数表达式,之后便可使用BP算法进行池化层的反向传播。
8.6 CIFAR10分类项目
使用卷积神经网络完成CIFAR10分类项目的具体代码如下:
import torch
import torch.nn as nn
from torchvision.datasets import CIFAR10
from torchvision.transforms import ToTensor, Compose
import torch.optim as optim
from torch.utils.data import DataLoader
import time
# 数据集构建
def create_dataset():
# 定义数据转换操作transform,
# Compose能够将多个转换操作组合在一起
# ToTensor()能够将图像数据转换为张量格式。
transform = Compose([ToTensor()])
train = CIFAR10(root='data', train=True, download=False, transform=transform)
test = CIFAR10(root='data', train=False, download=False, transform=transform)
return train, test
# 模型构建,一个继承,两个方法
# 继承父类nn.Module
class ImageClassifier(nn.Module):
# 定义__init__方法,初始化模型架构
def __init__(self):
# 定义卷积层、池化层、全连接层、BN层、以及激活函数
super(ImageClassifier, self).__init__()
self.conv1 = nn.Conv2d(3, 16,3, 1, 1)
self.bn1 = nn.BatchNorm2d(16)
self.conv2 = nn.Conv2d(16, 32, 3, 1, 1)
self.bn2 = nn.BatchNorm2d(32)
self.conv3 = nn.Conv2d(32, 64, 3, 1, 1)
self.bn3 = nn.BatchNorm2d(64)
self.conv4 = nn.Conv2d(64, 128, 3, 1, 1)
self.bn4 = nn.BatchNorm2d(128)
self.conv5 = nn.Conv2d(128, 256, 3, 1, 1)
self.bn5 = nn.BatchNorm2d(256)
self.conv6 = nn.Conv2d(256, 512, 3, 1, 1)
self.bn6 = nn.BatchNorm2d(512)
self.fc1 = nn.Linear(8192, 2048)
self.fc2 = nn.Linear(2048, 512)
self.fc3 = nn.Linear(512, 128)
self.fc4 = nn.Linear(128, 32)
self.out = nn.Linear(32, 10)
self.pool =nn.AvgPool2d(2, 2, 0)
self.activation = nn.ReLU()
# 定义forward方法,即网络的前向传播过程,两层卷积后加一层池化,最后使用全连接层进行分类。
def forward(self, x):
x = self.activation(self.bn1(self.conv1(x)))
x = self.activation(self.bn2(self.conv2(x)))
x = self.pool(x)
x = self.activation(self.bn3(self.conv3(x)))
x = self.activation(self.bn4(self.conv4(x)))
x = self.pool(x)
x = self.activation(self.bn5(self.conv5(x)))
x = self.activation(self.bn6(self.conv6(x)))
x = self.pool(x)
# 将一个batch中的每条输出数据展平为一维向量,第一维为batch_size
x = x.reshape(x.shape[0], -1)
x = self.activation(self.fc1(x))
x = self.activation(self.fc2(x))
x = self.activation(self.fc3(x))
x = self.activation(self.fc4(x))
return self.out(x)
# 模型训练,225原则
def train(model, train_dataset, epochs, batch_size, lr, gamma=0.9):
# 两个初始化,初始化损失函数对象和优化器对象
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=lr)
# 两轮循环,第一轮为epoch循环,第二轮为batch_size循环
for epoch in range(epochs):
# 创建数据加载器对象,主要作用为对数据进行分批和打乱
dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
tot_loss = cnt = 0
start = time.time()
print(f'正在第{(epoch + 1):4d}轮训练')
for x_train, y_train in dataloader:
# 五个步骤,前向传播、计算损失、梯度清零、反向传播,参数更新
y_pred = model(x_train.to('cuda'))
loss = criterion(y_pred, y_train.to('cuda'))
optimizer.zero_grad()
loss.backward()
optimizer.step()
tot_loss += loss.item()
cnt += 1
print(f'epoch:{(epoch + 1):4d}, loss:{(tot_loss / cnt):.5f}, time:{(time.time() - start):.3f}s')
# 保存模型的状态字典
torch.save(model.state_dict(), 'data/cifar10_model.pth')
# 模型预测三步:前向传播,获取预测结果,计算模型评估指标
def test(model, test_dataset, batch_size):
# 创建数据加载器对象,主要作用为对数据进行分批和打乱
dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
# 加载模型的状态字典
model.load_state_dict(torch.load('data/cifar10_model.pth'))
correct = 0
for x_test, y_test in dataloader:
y_pred = model(x_test.to('cuda')) # 前向传播
y_pred = torch.argmax(y_pred, dim=1) # 获取预测结果
correct += len(y_pred[y_pred == y_test.to('cuda')])
print(f'accuracy:{(correct / len(test_dataset)):.5f}') # 计算评估指标
if __name__ == '__main__':
# 定义超参数
epochs = 100
batch_size = 64
lr = 0.0005
# 数据集构建
train_set, test_set = create_dataset()
# 模型构建
model = ImageClassifier().to('cuda')
# 模型训练
train(model, train_set, epochs=epochs, batch_size=batch_size, lr=lr)
# 模型预测
test(model, test_set, batch_size=batch_size)