卷积神经网络基础篇(刘二大人PyTorch第十讲)
本节讲解了卷积相对于全连接神经网络的区别与优势、卷积神经网络进行图片特征提取的过程、卷积与下采样过程中通道数量与特征图张量大小的变化过程,最后搭建了一个简单的两层卷积,在MNIST数据集上进行性能测试,准确率比上一讲中全连接神经网络的准确率提升了1%。
全连接神经网络将图片张量展平为一维向量作为输入进行特征提取,存在的缺陷是,会损坏图片的空间结构(原本在空间位置上相邻的两个点在被展平后可能会相隔很远)。卷积神经网络是直接把图像张量作为输入,保存了图片原始的空间结构信息,卷积的实质可以看作是学习周围点对中心点的影响(只关注局部信息,不像全连接神经网络那样关注图片上任意两个点之间的影响)。
卷积神经网络进行图片分类的过程是:对于输入的图片张量(每张图片可以看作一个大的矩阵),经过卷积(可以看一个小矩阵,卷积的过程可以看作小矩阵在大矩阵上按一定步长滑动、对应位置点乘,学习周围点对中心点的影响)、下采样(关注局部范围内最显著的特征(最大池化)或平均特征(平均池化),减少神经元的数量,避免网络过于复杂、影响训练速度,同时避网络因为过于复杂而导致过拟合),最后得到多个通道的特征张量,将其展平为一维向量,接入全连接层进行分类。(对于分类任务,在最终分类层之前都是需要将所有神经元展平成一阶的一维向量(因为最后要做全连接进行分类))。

关于卷积核:
(1)每个卷积核的通道数必须与输入数据的通道数相同。在处理多通道数据时,卷积核要具有相同的通道数(一个通道配一个卷积)
(2)卷积核的数量与输入通道数相同。在卷积层中使用多个卷积核,每个卷积核会生成一个独立的特征图,通过增加卷积核的数量,可以让模型学到更多不同的特征。
(3)卷积核的大小自定义。卷积核大小的定义需要考虑感受野与任务需求、的匹配。
(4)卷积核的参数,(out_channel, in_channel, width, height),(输出通道数(卷积核的数量), 输入通道数(卷积核的通道数),卷积核的宽,卷积核的高)关于卷积层:
(1)卷积层要求输入输出是一个4维的张量(B, C, W, H)。输入:(batch_size, in_channel, width, height),输出 (batch_size, out_channel, width, height)。卷积后channel可变,width、height可变(取决于padding和是否下采样)。下采样后,channel不变,width、height变。关于全连接层:
全连接层的输入张量(batch_size, 特征数量),输出张量(batch_size,类别数量)。
在进行全连接层之前,需要将特征提取后得到的特征图展平为一维向量(使用.view(batch_size, features)进行展平,其中features的值需要推导)。再接入全连接层,计算得到属于各个类别的原始分数。
1. 自定义卷积核代码
import torch
input = [3,4,5,6,7,
2,4,6,8,2,
1,6,7,8,4,
9,7,4,6,2,
3,7,5,4,1]
input = torch.Tensor(input).view(1,1,5,5) # (batch_size, in_channel, width, height)
print(input.shape)
conv_layer = torch.nn.Conv2d(1,3,kernel_size=3,padding=1,bias=False) # conv2d参数(in_channel,out_channel,kernel_size,padding等)(in_channel与图像的输入通道一样,out_channel和卷积核的数量一样)
kernel = torch.Tensor([1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7,8,9]).view(3,1,3,3)
print(kernel.shape)
conv_layer.weight.data = kernel
output = conv_layer(input)
print(output.shape)
# 下采样
maxpooling_layer = torch.nn.MaxPool2d(kernel_size=2) # 核大小为2,步长也就默认为2
output = maxpooling_layer(output)
print(output)
说明:
(1)输入input是一个四维张量(batch_size,in_channel,width,height)
(2)nn.Conv2d用来管理每个卷积核及其对应的权重参数,其输入的数据张量形状是(batch_size, in_channel, width, height),输出张量(batch_size, out_channel, width, height),batch_size不变,out_channel改变(和卷积核数量一致),width, height改变(和padding、下采样有关)。nn.Conv2d的参数有(in_channel,out_channel, kernel_size, padding, stride, ...),其中,kernel_size可以是一个正方形,也可以是一个长方形(元组形式),如kernel_size=3,或kernel_size=(5,3)
(3)卷积核权重的定义:kernel = torch.Tensor([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27]).view(3,1,3,3)
卷积核的参数:(out_channel,in_channel, width, height)
这里在一个卷积层里定义了3个相同大小的、通道数为1的卷积核(在实际的训练过程中,卷积核中每个通道的参数模型自己学习的,所以卷积核也就是可学习的权重参数)。在大多数卷积神经网络的实现中,一个卷积层的多个卷积核大小是一样的,但在理论和一些特殊架构中也可以不同,如GoogLeNet(inception架构)就采用了不同大小卷积核并行的方式(但要注意,按通道的维度进行拼接时,每个分支最终输出的的批量大小、特征图大小必须一致才能拼接)
- 犯的错误:
不小心将torch.Tensor()写成了torch.tensor()。运行时的错误提示“TypeError: cross_entropy_loss(): argument 'input' (position 1) must be Tensor, not NoneType”。
原因:
关于torch.tensor 和 torch.Tensor
• torch.tensor:它是一个函数,用于根据传入的数据创建新的张量对象。你可以使用 Python 列表、NumPy 数组等作为输入,该函数会根据输入数据的类型和内容创建对应的张量。
• torch.Tensor:它是一个类,确切地说是 torch.FloatTensor 的别名。当你调用 torch.Tensor() 时,实际上是在创建一个 torch.FloatTensor 类型的张量对象,若不传入数据,会创建一个未初始化的张量。
当要将一个需要梯度计算的数据设置为张量时,数据类型必须是浮点型(如torch.float32、torch.float64)或者复数类型(如torch.complex64、torch.complex128)。
原因分析:这里的输入、卷积核的权重参数都得是浮点类型的张量,torch.Tensor会将输入数据自动转换为浮点型,写成torch.tensor后,torch.tensor识别出输入数据是整型,而整型不能不进行梯度计算,所以会报错)
2. 使用卷积神经网络进行图片分类代码
import torch
import torch.nn.functional as F
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import torch.optim as optim
# 1.加载数据
class CustomMNIST(datasets.MNIST):
mirrors = [
'https://ossci-datasets.s3.amazonaws.com/mnist/',
]
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,),(0.3081,))
])
batch_size = 64
train_data = CustomMNIST(root=r'./dataset/mnist/', train=True, download=True, transform=transform)
train_loader = DataLoader(train_data, shuffle=True, batch_size=batch_size)
test_data = CustomMNIST(root=r'./dataset/mnist/', train=False, download=True, transform=transform)
test_loader = DataLoader(test_data, batch_size=batch_size)
# 2. 模型设计
class Net(torch.nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = torch.nn.Conv2d(1, 10, kernel_size=5)
self.conv2 = torch.nn.Conv2d(10, 20, kernel_size=5)
self.pooling = torch.nn.MaxPool2d(kernel_size=2)
self.fc= torch.nn.Linear(320,10)
def forward(self, x):
batch_size = x.size(0) # x:(batch_size,in_channel,28,28)
x = F.relu(self.pooling(self.conv1(x))) # 卷积层输入是(B,C,W,H),输出是(B,C',W',H')
x = F.relu(self.pooling(self.conv2(x)))
x = x.view(batch_size, -1)
x = self.fc(x) # 全连接层的输入是(Batch_size,Feature), 输出是(Batch_size,Feature')
return x
model = Net()
# 3. 损失函数与优化器
criterion = torch.nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)
# 4. 训练与测试
def train(epoch):
running_loss = 0.0
for batch_idx, data in enumerate(train_loader, 0):
inputs, targets = data
optimizer.zero_grad()
# 正向,反向,更新
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
# 计算损失
running_loss = running_loss + loss.item()
if batch_idx %300 == 299:
print('epoch:%d, batch:%5d, loss:%.3f'%(epoch+1, batch_idx+1, running_loss/300))
def test():
correct, total = 0.0, 0.0
with torch.no_grad():
for data in test_loader:
images, labels = data
outputs = model(images)
_,predicted = torch.max(outputs.data, dim=1)
total = total + labels.size(0)
correct = correct + (predicted==labels).sum().item()
print('Accuracy on test set: %d %%'%(100*correct/total))
if __name__ == "__main__":
for epoch in range(10):
train(epoch)
test()
运行结果:
epoch:1, batch: 300, loss:0.622
epoch:1, batch: 600, loss:0.822
epoch:1, batch: 900, loss:0.958
Accuracy on test set: 96 %
epoch:2, batch: 300, loss:0.111
...
epoch:10, batch: 900, loss:0.108
Accuracy on test set: 98 %