目录
1.概述
1.1 卷积神经网络的引入
1.2 卷积神经网络的基本准则
1.2.1局部性
1.2.2相同性
1.2.3不变性
2.网络层次分析
2.1 卷积层
2.1.1滤波器的高度和宽度
2.1.2 步长
2.1.3 边界填充
2.1.4 卷积层代码实现
2.2 池化层
2.2.1池化层概述
2.2.2池化层实现
3.基于PyTorch的卷积神经网络对手写数字进行识别
在前一个博客中我们对于全连接神经网络做了一个实现。其中输入层有784个(28*28的图片)元素,四个隐藏层分别有400、300、200、100个神经元,输出层包括10个手写数字的类别,要表达这样一个很小的神经网络需要的权重约为:520200(784*400+300*200+200*100+100*10)个,如果每个权重使用4个字节的浮点数表示,这些权重会占用2080800字节,约为1.98MB,超出了当时计算机的内存大小。
于是,人们开始考虑能否将全连接神经网络的连接方式加以改变,这是前馈神经网络应运而生,卷积神经网络就是一个包含卷积运算且具有深度结构的前馈神经网络。卷积神经网络成功的原因在于其采用的局部连接和权值共享的方式,一方面减少了权值的数量,使得网络易于优化,另一方面降低了模型的复杂度,也就是减少了过拟合的风险,卷积神经网络在大型图像处理中有出色的表现。
指通过检测图片中的局部特征来决定图片的类别
指检测不同的图片是否具有相同的特征,虽然这些特征可能会出现在不同的地方,但是仍然可以通过局部特征来进行判断。
指对一张图片进行下采样时(对于一个样值序列,间隔几个样值取样一次,这样得到的新序列就是原序列的下采样),图片的性质基本保持不变
基于以上三个准则,典型的卷积神经网络至少有四个部分构成:输入层、卷积层、池化层以及全连接层。卷积层负责提取图片的局部特征;池化层用于大幅度降低参数的数量级(降维);全连接层类似传统的神经网络没用来输出预测的结果。
给定一个图像,CNN并不能准确的知道这些特征要匹配原图的哪些部分,所以会在原图中的每个可能的位置都进行尝试,相当于把这些特征当做一个滤波器(也称为卷积核)。这个用来匹配的过程就称为卷积。与全连接神经网络不同的是,卷积神经网络中每个神经元只与输入数据的一个局部区域连接,因为滤波器提取到的是局部特征。与神经元连接的空间大小(即感受视野的大小,即滤波器的宽度和高度)是需要人工设置的。
每个滤波器的工作就是在输入数据中寻找一种特征,每个滤波器的宽度和高度都比较小,但是深度和输入数据的深度保持一致。
步长表示每个滤波器每次移动的距离
卷积运算会使卷积图片的大小不断的变小,且由于图片左上角的元素只被一个输出使用,所以在图片边缘的像素,在输出中会被较少的使用,也就意味着很多的边缘信息会被丢失。为了解决这两个问题引入了边界填充(padding操作),也就是在图片卷积操作之前,沿着图片边缘用0进行边界填充。当步长等于1时,使用0填充能够使输入和输出的数据具有相同的空间尺寸。
设输入的图片尺寸为,步长为s,边界填充的大小为p,滤波器的尺寸为,则输出图片的尺寸为 其计算公式如下所示:
当输入图片的尺寸为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)
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()
池化层和卷积层一样,也是针对局部区域进行处理,池化层利用一个空间窗口(滤波器),通常取这些空间窗口中的最大值/加权平均值作为输出结果。然后不断滑动窗口,对输入图片的每个卷积操作结果进行单独处理,减小其尺寸空间。池化层的作用如下:
1.特征降维,避免过拟合
经过卷积操作的图片会含有非常多的特征,所以需要通过池化层对特征进行降维处理,池化处理也称为下采样。
2.空间不变性
池化层能在图片空间变化(旋转、压缩、平移)时而保持其特征不变。例如一张小狗的照片,像素很多时小狗很清晰,对图片进行压缩后,小狗变小了,但是仍然可以看出图片中是一个小狗,且其主要的特征没有变化。
3.减少参数,降低训练难度
池化处理一般分为最大池化和平均池化两种,最大池化就是在池化空间窗口取最大值,平均池化就是在池化空间窗口取加权平均值。
在PyTorch中常用nn.MaxPool2d()函数实现最大池化处理,该函数对于输入图片的要求与torch.nn.Conv2d()函数相同
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()
左图是在最大池化之前,右图是最大池化之后的结果。
可以看到经过经过池化后的图像仍然很清晰。
一共定义了五层,其中两层卷积层,两层池化层,最后一层为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()])
识别结果: