【深度学习】搭建卷积神经网络并进行参数解读
第一步 导包
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torchvision import datasets,transforms
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
transforms
模块是 torchvision
库的一部分,提供了多种图像变换方法,这些方法可以用于预处理和增强数据集中的图像。在训练深度学习模型时,对输入数据进行适当的预处理和增强是非常重要的步骤,它可以帮助提高模型的性能、加速收敛过程,并有助于防止过拟合。
以下是 transforms
模块中一些常用的变换方法及其用途:
-
transforms.ToTensor()
:-
将PIL Image或NumPy数组转换为PyTorch张量。
-
还会将图像像素值从[0, 255]缩放到[0.0, 1.0]范围内的浮点数。
-
-
transforms.Normalize(mean, std)
:-
对每个通道(如RGB)使用给定的均值和标准差对张量图像进行归一化。
-
例如,对于RGB图像,你可以提供一个均值列表(如
[0.485, 0.456, 0.406]
)和一个标准差列表(如[0.229, 0.224, 0.225]
),这两个值通常是基于ImageNet数据集计算得到的。
-
-
transforms.Resize(size)
:-
调整输入图像的大小到指定尺寸。
-
size
可以是一个整数(表示调整后的最小边长)或一个元组(表示宽和高的具体数值)。
-
-
transforms.CenterCrop(size)
:-
在中心裁剪出一个给定大小的区域。
-
size
可以是一个整数或一个元组,类似于Resize
。
-
-
transforms.RandomCrop(size)
:-
随机从图像中裁剪出一个给定大小的区域。
-
通常用于数据增强。
-
-
transforms.RandomHorizontalFlip(p=0.5)
:-
以给定的概率随机水平翻转图像(默认概率为0.5)。
-
这种变换可以帮助模型更好地泛化,因为它使得模型不会过度依赖于特定的方向性特征。
-
-
transforms.RandomRotation(degrees)
:-
随机旋转图像一定的角度。
-
degrees
参数指定了旋转的角度范围,比如(-45, 45)
表示在-45度到45度之间随机选择旋转角度。
-
-
transforms.ColorJitter(brightness=0, contrast=0, saturation=0, hue=0)
:-
改变图像的亮度、对比度、饱和度和色调。
-
各参数分别指定了各属性变化的最大幅度。
-
第二步 准备数据
# 定义超参数
input_size = 28 #图像的总尺寸28*28
num_classes = 10 #标签的种类数
num_epochs = 3 #训练的总循环周期
batch_size = 64 #一个撮(批次)的大小,64张图片
# 训练集
train_dataset = datasets.MNIST(root="./data",
train=True,
transform = transforms.ToTensor(),
download=True
)
# 测试集
test_dataset = 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
)
test_loader = torch.utils.data.DataLoader(dataset = test_dataset,
batch_size=batch_size,
shuffle=True
)
第三步 构建卷积神经网络模型
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(
in_channels=1,
out_channels=16,
kernel_size=5,
stride=1,
padding=2
),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2)
)
self.conv2 = nn.Sequential(
nn.Conv2d(16,32,5,1,2),
nn.ReLU(),
nn.Conv2d(32,32,5,1,2),
nn.ReLU(),
nn.MaxPool2d(2)
)
self.conv3 = nn.Sequential(
nn.Conv2d(32,64,5,1,2),
nn.ReLU()
)
self.out = nn.Linear(64*7*7,10)
def forward(self,x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = x.view(x.size(0),-1)
output = self.out(x)
return output
注1:
第一层卷积层:
-
nn.Conv2d
是一个二维卷积层:-
in_channels=1
:输入通道数为1,因为是灰度图像。 -
out_channels=16
:输出通道数为16,即生成16个特征图。 -
kernel_size=5
:卷积核大小为5x5。 -
stride=1
:步长为1。 -
padding=2
:填充2个像素,使得经过卷积后的尺寸保持不变。
-
-
nn.ReLU()
:激活函数,使用ReLU来增加非线性。 -
nn.MaxPool2d(kernel_size=2)
:最大池化层,窗口大小为2x2,步长默认与窗口大小相同,这将尺寸减半。
第二层卷积层:
包含两个 nn.Conv2d
层和一个 nn.MaxPool2d
层:
-
第一个
nn.Conv2d
将16个输入通道映射到32个输出通道。 -
第二个
nn.Conv2d
保持32个输入和输出通道不变。 -
每个卷积层后都有一个
nn.ReLU()
激活函数。 -
最后通过
nn.MaxPool2d(kernel_size=2)
进行最大池化操作,再次将尺寸减半。
第三层卷积层:
只包含一个卷积层和一个激活函数:
-
nn.Conv2d(32, 64, 5, 1, 2)
:从32个输入通道映射到64个输出通道。 -
nn.ReLU()
:应用ReLU激活函数。
全连接层:
定义了一个全连接层 nn.Linear
:
-
输入大小为
64 * 7 * 7
,这是因为最后一层卷积层输出的特征图大小为(64, 7, 7)
。 -
输出大小为10,对应于10个类别(对于MNIST数据集,这些类别是从0到9的数字)。
前向传播:
-
forward
方法定义了前向传播的过程:-
首先通过
self.conv1
、self.conv2
和self.conv3
对输入进行卷积和池化操作。 -
使用
x.view(x.size(0), -1)
将多维张量展平成二维张量,其中第一维保持不变(批次大小),第二维自动计算以适应展平后的数据大小。 -
最后通过
self.out
全连接层进行分类,并返回输出。
-
注2:
在 PyTorch 中,x.size(0)
用于获取张量 x
的第0维度的大小。PyTorch 张量的 .size()
方法返回一个 torch.Size 对象,它类似于 Python 的元组,包含了张量每个维度的大小。你可以通过索引访问各个维度的大小,比如 .size(0)
表示第一个维度(即第0维度)的大小。
具体解释:
假设你有一个形状为 (batch_size, channels, height, width)
的四维张量 x
,其中:
-
第0维度(
x.size(0)
)是批次大小(batch_size
)。 -
第1维度(
x.size(1)
)是通道数(channels
)。 -
第2维度(
x.size(2)
)是高度(height
)。 -
第3维度(
x.size(3)
)是宽度(width
)。
在这种情况下,x.size(0)
就会返回该张量的第一个维度(批次大小)的大小。
注3:
在卷积神经网络中,当你应用卷积操作时,图像的尺寸可能会发生变化。为了控制这种变化,并且在某些情况下保持输入和输出图像的尺寸相同,可以使用填充(padding)。对于一个给定的卷积层,如果你希望其输出尺寸与输入尺寸相同,可以通过选择合适的填充大小来实现。
假设输入图像的尺寸为 W×W(宽度和高度相等的情况),卷积核的大小为 F×F,步长(stride)为 S,填充(padding)大小为 P,则经过该卷积层后的输出尺寸 WoutWout 可以通过以下公式计算:
要使输出尺寸与输入尺寸相同(即 Wout=WWout=W),需要满足:
当步长 S=1 时,为了保持输出尺寸与输入尺寸相同,所需的填充 P 应该是 (F-1)/2,其中 F 是卷积核的大小。
注4:
nn.MaxPool2d(2)
是一个二维最大池化层,它通过对输入图像的每个局部区域应用最大操作来减少其空间尺寸(宽度和高度)。这个过程不仅有助于减少计算量和参数数量,还能在一定程度上控制过拟合。
注5:
在卷积神经网络(CNN)中,每次卷积操作之后通常会紧跟着一个激活函数,最常用的是ReLU(Rectified Linear Unit)。这背后有几个重要的原因:
1. 增加非线性
- **线性与非线性**:卷积操作本质上是线性的(即它包括加权求和),而大多数真实世界的数据(如图像)是非线性的。如果只使用线性变换,无论堆叠多少层,整个网络仍然只能表示线性映射。通过引入非线性的激活函数,可以使得网络能够学习和表达更复杂的模式和关系。
- **ReLU的作用**:ReLU函数定义为 \(f(x) = \max(0, x)\),它简单地将所有负值变为零,并保持正值不变。这种非线性变换允许网络学习更加复杂的功能,这是仅靠线性变换无法实现的。
2. 提升模型的表现
- **稀疏性**:ReLU的一个特点是它可以产生稀疏的激活。由于负值被置为零,这意味着并非所有神经元都会对最终输出有贡献,从而促进了稀疏性。稀疏性有助于提高计算效率,并且可能有助于防止过拟合。
- **梯度消失问题**:相比其他传统的激活函数如Sigmoid或Tanh,ReLU在正输入区域的导数恒等于1,这有助于缓解深层网络中的梯度消失问题。梯度消失是指在网络的反向传播过程中,梯度变得非常小,导致前面层的学习速度极慢甚至停止学习。ReLU的存在可以在一定程度上缓解这一问题,特别是在深度网络中。
3. 计算效率
- **计算简便**:ReLU的计算非常简单,只需要比较每个元素是否大于零并保留该值或者将其设为零。相比之下,像Sigmoid或Tanh这样的激活函数需要进行指数运算,这在计算上更为昂贵。
- **硬件友好**:由于其简单的形式,ReLU非常适合现代硬件加速器(如GPU、TPU等)上的并行计算,进一步提高了训练和推理的速度。
4. 实践证明有效
- **经验验证**:大量研究表明,在许多任务中,使用ReLU作为激活函数可以获得比传统激活函数更好的性能。尤其是在图像识别领域,ReLU已经成为默认的选择之一。
总结
每次卷积后加入ReLU的主要原因是增加非线性、提升模型表现以及提高计算效率。通过这些方式,ReLU帮助构建了强大且高效的深度学习模型。虽然ReLU有许多优点,但也有其局限性,例如对于负输入的“死亡”ReLU问题(某些神经元永远不会再激活),因此近年来也出现了一些ReLU的变种,如Leaky ReLU、Parametric ReLU等,以解决这些问题。不过,即使存在这些问题,ReLU仍然是目前最广泛使用的激活函数之一。
第四步 定义准确率计算函数
def accuracy(predictions, labels):
# 使用torch.max找到predictions中每个输入的最大值的索引,
# 即预测的类别。torch.max会返回两个张量,第一个是最大值,
# 第二个是该最大值对应的索引,所以我们选择[1]来获取索引。
pred = torch.max(predictions.data, 1)[1]
# eq比较两个张量是否相等,view_as确保labels被调整到与pred相同的形状。
# 注意原代码中的`view_as(pred)`写错了,应该是`.view_as(pred)`。
rights = pred.eq(labels.data.view_as(pred)).sum().item()
# 返回正确的预测数量和总的标签数量
return rights, len(labels)
第五步 训练卷积神经网络模型
# 实例化
net = CNN()
# 损失函数
criterion = nn.CrossEntropyLoss()
# 优化器
optimizer = optim.Adam(net.parameters(),lr=0.001)
#开始训练
for epoch in range(num_epochs):
train_rights = []
for batch_idx,(data,target) in enumerate(train_loader):
net.train()
output = net(data)
loss = criterion(output,target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
right = accuracy(output,target)
train_rights.append(right)
if batch_idx % 100 ==0:
net.eval()
val_rights = []
for (data,target) in test_loader:
output = net(data)
right = accuracy(output,target)
val_rights.append(right)
#准确率计算
train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))
val_r = (sum([tup[0] for tup in val_rights]), sum([tup[1] for tup in val_rights]))
print('当前epoch: {} [{}/{} ({:.0f}%)]\t损失: {:.6f}\t训练集准确率: {:.2f}%\t测试集正确率: {:.2f}%'.format(
epoch, batch_idx * batch_size, len(train_loader.dataset),
100. * batch_idx / len(train_loader),
loss.data,
100. * train_r[0].numpy() / train_r[1],
100. * val_r[0].numpy() / val_r[1]))
训练模型部分:
-
for epoch in range(num_epochs):
:对于每个epoch进行循环。 -
train_rights = []
: 初始化一个列表来存储每一批次的正确预测数量和总样本数。 -
for batch_idx, (data, target) in enumerate(train_loader)
: 遍历训练数据加载器,每次获取一批(batch)的数据和标签。 -
net.train()
: 设置模型为训练模式,这对于使用如Dropout或BatchNorm等层很重要。 -
output = net(data)
: 前向传播,将输入数据通过网络得到输出。 -
loss = criterion(output, target)
: 计算预测输出与实际标签之间的损失。 -
optimizer.zero_grad()
: 清除之前的梯度信息,准备进行新的反向传播计算。 -
loss.backward()
: 反向传播,计算每个参数的梯度。 -
optimizer.step()
: 根据计算得到的梯度更新模型参数。 -
right = accuracy(output, target)
: 调用自定义的accuracy
函数,计算当前批次的准确率。 -
train_rights.append(right)
: 将当前批次的准确率结果添加到train_rights
列表中。
测试模型部分:
-
if batch_idx % 100 == 0:
: 每处理100个batch后执行以下代码块,用于评估模型在验证集上的性能。 -
net.eval()
: 设置模型为评估模式,关闭如Dropout等功能以确保评估时的行为正确。 -
val_rights = []
: 初始化一个列表来存储验证集上每一批次的正确预测数量和总样本数。 -
for (data, target) in test_loader:
: 遍历测试数据加载器,每次获取一批(batch)的数据和标签。 -
output = net(data)
: 前向传播,将输入数据通过网络得到输出。 -
right = accuracy(output, target)
: 计算当前批次的准确率。 -
val_rights.append(right)
: 将当前批次的准确率结果添加到val_rights
列表中。 -
train_r
和val_r
: 分别计算训练集和验证集上的累计正确预测数和总样本数。 -
print(...)
: 打印当前epoch的信息,包括:-
当前epoch编号。
-
已经处理了多少个样本(
batch_idx * batch_size
),总共多少个样本(len(train_loader.dataset)
),以及这个比例(百分比形式)。 -
当前batch的平均损失值。
-
训练集和验证集上的准确率(百分比形式)。注意这里
loss.data
可能需要根据PyTorch版本调整为loss.item()
,因为.data
属性在新版本中已被弃用。
-
卷积神经网络模型构建至此结束。