深度学习作业 - 作业七 - 基于CNN的XO识别
一、认识概念-用自己的语言描述
这里所有的概念均经过查阅资料后进行了吸收与总结,都是个人理解。
局部感知
在学习局部感知之前,我们先抛弃感知不谈,来了解一下局部是什么含义。
如下图所示的情况,(a)图表示,神经网络中的神经元是全连接的一种结构,全连接则代表着对于神经网络任意相邻的两层而言,神经元都是彼此互相连接的,后面一层中的神经元可以完全获取前面一层的所有神经元信息。
而图(b)则表示的是神经元之间的局部连接,后面一层中的每个神经元只能获取部分来自于前面一层的信息,而不是完全获取。

人眼感知图像的时候并不是依靠全连接,而是每个神经元只负责特定的区域(局部感受野),通过在更高层将这些感受不同局部区域的神经元综合起来就可以得到全局图像信息。
而在局部连接中的神经元获取信息的方式,称为局部感知。前面作业中我们已经提到过感受野了,在局部感知下,每一个神经元的感受野都不相同。局部感知意味着每个神经元只关注输入数据中的局部区域,而不是处理整个输入。
局部感知的好处
可能在这里,我们会产生疑问说局部感知每个神经元只能感知一部分信息,会丢失很多信息的吧。其实则不然,每个神经元其实不需要关注整张图像上的信息,只要关注一部分的信息,然后将所有信息合在一起,效率会更高。因此,应用了局部感知后,模型的特征提取能力反而会上升。
- 减少计算的复杂度:通过局部感知这一操作,需要学习的参数数量大大下降,每个神经元原来需要看整个图片一遍,现在只需要看属于自己的一部分就行了。这样一来,模型可以更有效地学习复杂模式,同时也可以减少在卷积过程中积累的局部信息。
- 提高模型特征提取能力:将原先需要整体学习的整体知识拆分为数个小区域,从小区域提取特征,再逐层结合这些局部特征,模型能够更有效地学习复杂的特征模式。
权值共享
平移不变性
不管观察对象在空间的哪个地方,网络仍能将其外表特征识别出来,而不受位置变换的影响。

权重共享意味着网络模型在对输入的图片进行卷积操作时,对于相同特征的提取,卷积核的参数是共享的,即所有的卷积核共同使用一套参数进行特征提取。相当于一个全图的滤波。
这样一来,所有隐层神经元共用一种特征观察模式,即具有了平移不变性。
例如,在次作业中对人物面部特征进行提取,若要提取鼻子的特征,我们所有的卷积核都利用与鼻子有关的特征进行特征提取。这种特征也可以减少卷积神经网络需要学习的参数数量。
局部感知+权值共享
CNN用局部感知和权值共享大大减少了参数,同时还具备其它优点。它们与自然图像自身具有的特性:特征的局部性与重复性完美贴合。
若我们想提取不同的特征,则可以设置一系列不同权重的卷积核通过局部感知+权重共享的方式来进行相关特征的提取,这样的特征提取模式得到的特征要比全连接提前更加简便与精细。
池化(子采样、降采样、汇聚)
在网络进行前向传播的过程中,考虑到图像中存在的冗余信息,可以用某一个区域子块中的统计信息来刻画该部分所有像素点的共同特征,这就是池化操作(也称为子采样,降采样,汇聚)。
池化操作就是对已得到的特征进行进一步采样和降维,在实现了下采样的同时保证了特征图的主要信息。
最大池化与平均池化
最大池化与平均池化是池化过程中的两个常见方法。
平均池化的原则是计算输入特征图某个区域子块中所有像素点的均值,将这个均值作业平均池化结果。如下图(a)。
最大池化的原则是从输入特征图的某个区域子块中选择最大的像素点作为最大池化结果,如下图(b)。

具体的池化大小、尺寸等都可以在模型中进行调试,计算方法可见池化(Pooling)-CSDN博客。
池化带来了什么好处与坏处?
- 池化通过降低特征图的尺寸,可以减少后续层的计算量与参数量,降低了模型的复杂度,从而加快模型的训练与推理,同时还可以降低模型的过拟合风险。
- 前面我们提到特征的平移不变性,通过池化操作,模型对输入图像的小平移、旋转都有了一定的鲁棒性,从而提供了泛化能力。
- 池化有助于优化特征信息,保留重要的特征,滤除一些不需要的细节信息,使特征具有代表性。
- 在下采样的过程中,由于池化操作,会丢失一些细节信息,这在对细节学习要求比较高的任务中会导致性能下降。模型将无法很好的捕捉局部特征的关系与细节。
- 在某些与位置相关比较大的任务中,如物体定位,经过池化的模型位置敏感性会降低,导致模型定位困难。
- 池化操作可能对梯度传播产生影响,导致某些特征的梯度被稀释,从而影响模型的学习。
全卷积网络
CNN:传统的CNN网络会在卷积层的最后连接上若干个全连接层,将卷积层产生的特征图将卷积层产生的特征图映射成为一个固定长度的特征向量,如下图所示。

FCN: FCN是对图像进行像素级的分类(也就是每个像素点都进行分类),从而解决了语义级别的图像分割问题。如下图所示。

FCN的原理就是采用反卷积层在最后一个卷积层的特征图上进行上采样,使它恢复成一个与输入图像尺寸相同的图,从而对原有输入图的每一个像素产生一个预测。
全卷积网络,顾名思义就是全部都是卷积层的网络。简单地说,全卷积网络就是把卷积神经网络最后的全连接层变成了卷积层,输出的是一张已经标记好相关性的热力图,而不是一个概率值。
- 由于不含全连接层,只有卷积层,全卷积网络可适应任意尺寸输入。
- 全卷积网络在最后通过反卷积层增大图像尺寸,输出精细结果。
- 全卷积网络通过结合不同深度层结果的跳级结构,确保鲁棒性和精确性。
低级特征、中级特征、高级特征
低级特征:低级特征是从浅层网络中提取出来的,指的是图像中一些较为基础的、直接能够从原始数据中提取的特征,如颜色、纹理、边缘等,他们通常不具备很强的表达能力,但是可以作为中级特征的基础。
中级特征:中级特征则是基于低级特征构建的一些更高层次的特征(抽象与组合),比如形状、轮廓、纹理组合等,这些特征能更好的描述物体的局部形态,具备一定的表达能力。
高级特征:高级特征是从深层网络中提取出来的,是基于中级特征构建的一系列更加抽象与复杂的特征,如物体的部件、结构、语义等,这些特征能够更好的描述物体的高层次语义信息,因此适用于更加复杂的任务,表达能力极强。

如图所示,从左到右依次是低级特征、中级特征、高级特征。可以看到,从低级特征到高级特征,特征是越来越细致、越来越抽象,同时这些特征的表达能力也逐渐增强,语义层次逐渐变高。
浅层网络与深层网络
浅层网络感受野较小,更加注重细节信息;深层网络感受野较大,更加注重全局信息。
在进行浅层特征提取时,能够利用更多的细粒度特征信息,而且此时特征图每个像素点对应的感受野重叠区域还很小,这就保证了网络能够捕获更多细节。浅层特征具有更高的分辨率。
在进行深层特征提取时,随着下采样或卷积次数增加,感受野逐渐增加,感受野之间重叠区域也不断增加,此时的像素点代表的信息是一个区域的信息,获得的是这块区域或相邻区域之间的特征信息,相对不够细粒度、分辨率较低,但语义信息丰富。
多通道,N输入,M输出是如何实现的?
多通道
我们首先来解释一下多通道是什么意思。通道指的是特征图的深度维度。每个通道代表了一组特征,通常是在某一层通过卷积操作提取的不同特征。
例如,对于彩色图像,通常有三个通道:红色通道、绿色通道和蓝色通道(RGB)。
多输入单输出
在卷积的时候,由于有三个通道,每个通道相对应的特征权重也不同,那么我们就需要同样带有通道维度的卷积核来针对不同的通道进行卷积操作。

如上图展示了一个5x5x3的输入特征图在一个3x3x3的卷积核的卷积下分别得到了关于每个特征通道的特征图,最后我们将三个通道的特征图相加得到了一个3x3x1的特征图,这就算是完成了一次多通道卷积。将一个三通道的输入卷积为了一个一通道的特征图。

多通道输入在经过卷积操作后(卷积核的通道数需和输入通道数保持一致),得到了通道数为1的特征图。
多输入多输出
上面我们介绍了多通道输入,1输出的原理,下面我们学习多输入多输出,即N输入M输出是怎么实现的。
我们知道,每对一个多通道输入的特征图进行一次卷积,就能得到一个单通道的输出特征图,若是我们想要得到多特征图的输出,我们就可以对原图进行多次多通道卷积,若是想要得到M通道的输出,只需要对原输入进行M次多通道卷积即可。如下图所示。

举个例子,比如我有一个输入特征图,维度为5x5xN,我想对这个特征中的M个不同方面的特征进行特征提取,那么我就需要设计M组不同的含有N个通道的卷积核,来针对该输入的不同特征进行卷积操作,最终将所有卷积得到的特征图进行整合就得到了M维度的输出。这就是N输入M输出的原理。
1x1卷积核有什么用?
1x1卷积是什么
1x1卷积,与标准的卷积完全是一样的,唯一的区别就在1x1卷积的卷积核尺寸是1x1。我们思考一下,使用1x1的卷积核进行卷积,我们只需要对每一个元素乘上相关的权重就行了,不需要考虑输入的数据之间的权重影响关系。这在单通道卷积中看似没什么大不了的,实际上在多通道卷积上大有作用。

如图, 当输入为3x3x3的特征图时,我们使用4个1x1卷积核进行卷积计算,最终得到的输出就会与输入尺寸相同,并且通道数等于我们使用的1x1卷积核通道数。
作用
1.调节通道数
由于1x1的卷积核不会改变原输入特征图的尺寸,所以我们可以通过控制1x1卷积核的通道数,来调节输入特征图的通道数,得到与卷积核通道数相同的原尺寸的新输入特征图。
2.增加非线性
可以利用1x1卷积之后的非线性激活函数如ReLU,使得卷积之后的特征图非线性大大增强。非线性越强,网络可以学到的功能就越复杂,整个网络也能够进一步加深。
3.跨通道信息
1x1卷积核只有一个参数,当它作用在多通道的feature map上时,相当于不同通道对应位置的一个线性组合,但是这样输出的feature map就是多个通道的整合信息了,能够使网络提取的特征更加丰富。
4.减少参数量
1x1卷积核在降低输入特征图的通道数量的同时,也减少了相应通道的参数,相当于在特征图的通道数上进行卷积,压缩特征图,二次提取特征,使得新的特征图表现更好。

如上图所示,同样维度的输入特征图,直接进行卷积与经过一个1x1卷积核之后再进行卷积,需要模型学习的参数就少了很多。
1x1卷积在ResNet中的应用–残差网络

在残差网络中,为了在增加模型复杂度的同时,不让模型学习的参数也变得很多,采用了1x1卷积的方式降低了新网络的通道数,从而减少了模型参数,在不改变模型表达能力的前提下,大大减少所使用的参数量。
二、使用CNN进行XO识别
1.复现参考资料中的代码
代码
研究老师给出的代码,并进行代码的复现工作,以下是我研究后并加入注释的代码。
数据读取+模型训练+模型测试+模型评价
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
import matplotlib.pyplot as plt
import numpy as np
import torch.nn as nn
import torch
# -------------------------------------------------------读取数据---------------------------------------------------------
transforms = transforms.Compose([
transforms.ToTensor(), # 把图片进行归一化,并把数据转换成Tensor类型
transforms.Grayscale(1) # 把图片 转为灰度图
])
# 使用 PyTorch的datasets.ImageFolder类来加载一个图像数据集
data_train = datasets.ImageFolder('training_data_sm', transforms)
data_test = datasets.ImageFolder('training_data_sm', transforms)
# 创建数据集加载器,batch_size为每个批次处理的数据数目,shffle意为随机打乱
train_loader = DataLoader(data_train, batch_size=64, shuffle=True)
test_loader = DataLoader(data_test, batch_size=64, shuffle=True)
# 输出训练集和测试集的维度
for i, data in enumerate(train_loader):
images, labels = data
print(images.shape)
print(labels.shape)
break
for i, data in enumerate(test_loader):
images, labels = data
print(images.shape)
print(labels.shape)
break
# ------------------------------------------------------可视化数据--------------------------------------------------------
a = 0
plt.figure(figsize=(15, 5))
index = 0
for i in labels:
if i == 0 and a < 5:
plt.subplot(151 + a)
plt.imshow(images[index].data.squeeze().numpy(), cmap='gray')
plt.title('circle ' + str(a + 1))
a += 1
if a == 5:
break
index += 1
plt.show()
a = 0
plt.figure(figsize=(15, 5))
index = 0
for i in labels:
if i == 1 and a < 5:
plt.subplot(151 + a)
plt.imshow(images[index].data.squeeze().numpy(), cmap='gray')
plt.title('crosses ' + str(a + 1))
a += 1
if a == 5:
break
index += 1
plt.show()
# -------------------------------------------------------构建模型---------------------------------------------------------
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(1, 9, 3) # 输入通道1,输出通道9
self.pool = nn.MaxPool2d(2, 2) # 池化窗口大小2x2
self.conv2 = nn.Conv2d(9, 5, 3) # 输入通道9,输出通道5
self.relu = nn.ReLU()
self.fc1 = nn.Linear(27 * 27 * 5, 1200)
self.fc2 = nn.Linear(1200, 64)
self.fc3 = nn.Linear(64, 2)
def forward(self, x):
# 卷积->激活->池化->卷积->激活->池化->展平->全连接->激活->全连接->激活->全连接
x = self.pool(self.relu(self.conv1(x)))
x = self.pool(self.relu(self.conv2(x)))
x = x.view(-1, 27 * 27 * 5)
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.fc3(x)
return x
# -------------------------------------------------------训练模型---------------------------------------------------------
model = CNN() # 实例化模型
loss = nn.CrossEntropyLoss() # 交叉熵损失
opti = torch.optim.SGD(model.parameters(), lr=0.1) # 优化器
epochs = 10 # 训练10轮
for epoch in range(epochs):
total_loss = 0 # 总损失
for i, data in enumerate(train_loader):
images, labels = data
out = model(images)
one_loss = loss(out, labels)
opti.zero_grad()
one_loss.backward()
opti.step()
total_loss += one_loss
if (i + 1) % 10 == 0:
print(
f'训练轮次进度[{epoch + 1}/{epochs}]-训练数据进度[{i + 1}]-当前损失为{total_loss / 100 :.3f}')
total_loss = 0.0
print('训练完成')
# 保存模型
torch.save(model, 'model.pth') # 保存的是模型,不止是w和b权重值
# -------------------------------------------------------测试模型---------------------------------------------------------
model_load = torch.load('model.pth')
# 读取一张图片 images[0],测试
print("labels[0] truth:\t", labels[0])
x = images[0].unsqueeze(0)
predicted = torch.max(model_load(x), 1)
print("labels[0] predict:\t", predicted.indices)
img = images[0].data.squeeze().numpy() # 将输出转换为图片的格式
plt.imshow(img, cmap='gray')
plt.show()
# -------------------------------------------------------模型评价---------------------------------------------------------
model_load = torch.load('model.pth')
correct = 0
total = 0
with torch.no_grad(): # 进行评测的时候网络不更新梯度
for data in test_loader: # 读取测试集
images, labels = data
outputs = model_load(images)
_, predicted = torch.max(outputs.data, 1) # 取出最大值的索引作为分类结果,第一个返回参数为预测分数,不需要,可以赋值为空行
total += labels.size(0) # labels 的长度
correct += (predicted == labels).sum().item() # 预测正确的数目
print('在测试集上的准确率为: %f %%' % (100. * correct / total))
关于数据集加载
datasets.ImageFolder是一个用于加载图像数据集的类。它会自动根据目录结构组织数据,目录的名称视为标签。
例如,在本次加载的数据集中,有ciecles_sm与crosses_sm两个类别,数据集加载器便将第一个文件夹中的数据标记为0,将第二个文件中的数据标记为1。
关于展平层Flatten的计算
展平层得到的数据是27x27x5,详细计算方式需要参照经过卷积后的特征图的尺寸计算方式。
简明易懂——卷积神经网络的输入输出特征图大小计算_卷积输入输出大小计算-CSDN博客
这里直接给出计算结果:

关于卷积结果的取整问题
第一次卷积后size=116-3+1=114,第一次池化后size=114/2=57
第二次卷积后size=57-3+1=55,第二次池化后55/2=27.5
这个27.5应该算27还是28呢?
关于这个问题,已经有学长在自己的博客中印证了,若是选择28,则最后的结果会报错。因此,在Pytorch中,参数的选择为向下取整原则。
运行结果与分析
这里贴一张老师作业中放的模型结构图,我们本次作业复现的就是这个结构的模型。

数据集介绍

XO数据集,分别有圆圈与叉子各1000张图片。

读取数据集

这张图展现了训练集与测试集中图像数据的shape属性,上面两条为训练集,下面两条为测试集,本次实验直接将训练集全体拿来做了测试集。其中这四个维度分别表示:
第一维:表示批次大小(batch size),即此模型将要处理的图像数。
第二维:表示通道数目,即传入图片的通道数,对于彩色图有RGB三个通道,对于灰度图只有一个通道。
第三位、第四维:表示图像数据的,高度与宽度。
拿第一个数据举例,这是一批有64张图像,通道为1,高度与宽度分别为116与116的图像数据。
可视化数据集
从数据集中选取5张样本图片进行展示,如下图所示:


模型训练

可以看到,随着模型的训练,损失逐渐下降,最终几乎为0。训练的还是不错的。
模型测试
随便选取一张图片,放进模型进行预测,将得到的预测结果与原标签输出出来进行对比,如图所示。

模型评估
将模型转化为测试模式,在测试模式下,不更新网络的梯度,将测试集输入模型,得到整体的输出标签,将测试标签与真实标签进行一一对应,计算准确率。
![]()
得到的准确率高达99.9%,模型训练很成功!
至此,我们模型的复现部分就全结束了,复现出来的模型效果还是不错的。
2.重新设计网络结构
增加卷积层

网络的更改主要是基于我们刚才复现的代码的网络结构,主要变动为在展平层之前增加一个[卷积-激活-池化]流程,使得卷积层变为三层。
但是改动不止要有这些,前面我们了解了展平层参数的计算由来,由于网络结构改变了,我们同时还需要重新更改展平层的参数与全连接层第一层的输入参数。
第一次卷积后size=116-3+1=114,第一次池化后size=114/2=57
第二次卷积后size=57-3+1=55,第二次池化后55/2=27.5(下取整变为27)
第三次卷积后size=27-3+1=25,第三次池化后size=25/2=12.5(下取整变为12)
代码如下
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(1, 9, 3) # 输入通道1,输出通道9
self.pool = nn.MaxPool2d(2, 2) # 池化窗口大小2x2
self.conv2 = nn.Conv2d(9, 5, 3) # 输入通道9,输出通道5
self.conv3 = nn.Conv2d(5, 4, 3) # 输入通道5,输出通道4
self.relu = nn.ReLU()
self.fc1 = nn.Linear(12 * 12 * 4, 1200) # 改变展平层参数
self.fc2 = nn.Linear(1200, 64)
self.fc3 = nn.Linear(64, 2)
def forward(self, x):
# 卷积->激活->池化->卷积->激活->池化->展平->全连接->激活->全连接->激活->全连接
x = self.pool(self.relu(self.conv1(x)))
x = self.pool(self.relu(self.conv2(x)))
x = self.pool(self.relu(self.conv3(x))) # 增加了一个流程循环
x = x.view(-1, 12 * 12 * 4) # 改变展平层参数
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.fc3(x)
return x
训练日志与结果

从训练过程来看,增加一个卷积流程,无论在损失下降速度还是在损失下降效果来看,均优于不添加这个卷积流程的情况。
从模型测试来看,增加一个卷积流程,原先的准确率99.9%变为了如今的100%。
可见,增加这个卷积层对模型的性能是有不错的提升的。
去掉池化层

去除掉池化层后,我们首先需要计算新的展平层与第一个全连接层的各个参数。
第一次卷积后size=116-3+1=114
第二次卷积后size=114-3+1=112
第三次卷积后size=112-3+1=110
按照这个参数修改代码,修改后的代码如下
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(1, 9, 3) # 输入通道1,输出通道9
self.conv2 = nn.Conv2d(9, 5, 3) # 输入通道9,输出通道5
self.conv3 = nn.Conv2d(5, 4, 3) # 输入通道5,输出通道4
self.relu = nn.ReLU()
self.fc1 = nn.Linear(110 * 110 * 4, 1200) # 改变展平层参数
self.fc2 = nn.Linear(1200, 64)
self.fc3 = nn.Linear(64, 2)
def forward(self, x):
# 去除pool层
x = self.relu(self.conv1(x))
x = self.relu(self.conv2(x))
x = self.relu(self.conv3(x)) # 增加了一个流程循环
x = x.view(-1, 110 * 110 * 4) # 改变展平层参数
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.fc3(x)
return x
训练日志与结果

与上一次运行的结果相对比来看,损失值卡到了0.069无法继续下降,并且没有收敛。
准确率降低到了50%,模型训练效果并不理想。
去除池化层后,模型的参数量太大,我们前面已经写到,池化层的作用是在保持主要特征表达能力不削减、保留重要信息的前提下,降低模型的复杂度,减少需要训练的参数。因此,我们的模型崩溃了,没有发挥其应有的作用。
这给了我们一个启发:在我们实际应用的时候,需要搭配好卷积层与池化层的配比与尺寸,以使得模型在最优的情况下进行训练,至于怎么调试,就要靠我们日后的经验积累了。
修改通道数等超参数
基于有池化层与三层卷积层的程序,我们将原来的网络结构中三个卷积层的通道数随机修改为如下。

由前面的计算过程我们可以得知,通道数的多少与展平层的参数并没有关系。因此在这里,我们不需要更改展平层与第一个全连接层的参数,只需要修改通道数即可。
代码如下
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(1, 6, 3) # 输入通道1,输出通道6
self.pool = nn.MaxPool2d(2, 2) # 池化窗口大小2x2
self.conv2 = nn.Conv2d(6, 3, 3) # 输入通道6,输出通道3
self.conv3 = nn.Conv2d(3, 2, 3) # 输入通道3,输出通道2
self.relu = nn.ReLU()
self.fc1 = nn.Linear(12 * 12 * 2, 1200) # 改变展平层参数
self.fc2 = nn.Linear(1200, 64)
self.fc3 = nn.Linear(64, 2)
def forward(self, x):
# 卷积->激活->池化->卷积->激活->池化->展平->全连接->激活->全连接->激活->全连接
x = self.pool(self.relu(self.conv1(x)))
x = self.pool(self.relu(self.conv2(x)))
x = self.pool(self.relu(self.conv3(x))) # 增加了一个流程循环
x = x.view(-1, 12 * 12 * 2) # 改变展平层参数
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.fc3(x)
return x
进行训练之后得到的训练结果如下。

对比之前的训练日志来看,更改了通道数之后模型收敛效果差,预测精度高,接下来我们就通道数对模型的影响来对训练结果进行分析。
通道数有什么作用?
1.增强特征表示能力:更多的通道数往往代表着更强大的模型特征表示能力,模型能够捕捉更加复杂的模型与细节。
2.模型的复杂度:更多的模型通道数也意味着模型的参数数量增加,复杂度直线上升,计算开销与训练推理时长延长,同时模型容易达到过拟合。
3.优化卷积操作:卷积操作是一种非常计算密集的操作,通道数的增加可以优化卷积操作的效率。当卷积核通道数较多时,可以利用硬件并行化的特性,使卷积操作更加高效。
通道数就像其他超参数一样,需要在多与少之间找到一个平衡点,更多的通道数代表着更细致的网络,同时也代表着更大的计算开销,在实际应用时,需要找到一个模型复杂度与计算资源之间的平衡,以使得模型发挥最大的性能。
这里贴一个知乎大佬的总结在卷积神经网络中,channels作用是什么,channels数量越多是否提取上下文能力越强?
3.可视化
可视化部分卷积核与特征图
特征图可视化
由于在作业中老师提到需要使用最优模型来可视化,这里就选择了带有池化层与三个卷积流程的模型,其在测试的时候达到的准确率为100%,具体实现代码如下。
import torch.optim
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
import matplotlib.pyplot as plt
import numpy as np
import torch.nn as nn
transforms = transforms.Compose([
transforms.ToTensor(), # 把图片进行归一化,并把数据转换成Tensor类型
transforms.Grayscale(1) # 把图片 转为灰度图
])
data_train = datasets.ImageFolder('training_data_sm', transforms)
data_test = datasets.ImageFolder('training_data_sm', transforms)
train_loader = DataLoader(data_train, batch_size=64, shuffle=True)
test_loader = DataLoader(data_test, batch_size=64, shuffle=True)
for i, data in enumerate(train_loader):
images, labels = data
print(images.shape)
print(labels.shape)
break
for i, data in enumerate(test_loader):
images, labels = data
print(images.shape)
print(labels.shape)
break
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(1, 9, 3)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(9, 5, 3)
self.relu = nn.ReLU()
self.fc1 = nn.Linear(27 * 27 * 5, 1200)
self.fc2 = nn.Linear(1200, 64)
self.fc3 = nn.Linear(64, 2)
def forward(self, x):
outputs = []
x = self.conv1(x)
outputs.append(x)
x = self.relu(x)
outputs.append(x)
x = self.pool(x)
outputs.append(x)
x = self.conv2(x)
x = self.relu(x)
x = self.pool(x)
x = x.view(-1, 27 * 27 * 5)
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.fc3(x)
return outputs
# ----------------------------------------------------可视化特征图---------------------------------------------------------
# 读取模型
model = torch.load('model.pth')
print(model)
x = images[0].unsqueeze(0)
# forward正向传播过程
out_put = model(x)
for feature_map in out_put:
# [N, C, H, W] -> [C, H, W] 维度变换-N表示批次大小,C表示通道数,H表示高度,W表示宽度
im = np.squeeze(feature_map.detach().numpy())
print(im.shape)
# [C, H, W] -> [H, W, C]
im = np.transpose(im, [1, 2, 0])
print(im.shape)
# 展示九个特征图
plt.figure()
for i in range(9):
ax = plt.subplot(3, 3, i + 1) # 参数意义:3:图片绘制行数,5:绘制图片列数,i+1:图的索引
# [H, W, C]
# 特征矩阵每一个channel对应的是一个二维的特征矩阵,就像灰度图像一样,channel=1
# plt.imshow(im[:, :, i])
plt.imshow(im[:, :, i], cmap='gray')
plt.show()
代码主要是将输出的所有特征图存储在了output列表中,随后分别展示了卷积后-池化后-激活后的前9个特征图,得到的结果如下图所示。




可以看到,在卷积后模型捕捉到了图像中一些边缘或特征边界,背景区域和目标边缘之间的差异比较明显。在池化后,图像分辨率变低,内容显得更加模糊和紧凑,但重要的特征仍然保留。在激活后,特征图比池化后的特征图更加清晰,突出了一些强度较大的特征或边缘。网络更有效地捕捉到了非线性特征。
卷积核可视化
将模型读取之后,分别识别卷积核1与卷积核2,再输出两者所有的卷积核通道特征图,代码如下。
import torch.optim
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
import matplotlib.pyplot as plt
import torch.nn as nn
transforms = transforms.Compose([
transforms.ToTensor(), # 把图片进行归一化,并把数据转换成Tensor类型
transforms.Grayscale(1) # 把图片 转为灰度图
])
data_train = datasets.ImageFolder('training_data_sm', transforms)
data_test = datasets.ImageFolder('training_data_sm', transforms)
train_loader = DataLoader(data_train, batch_size=64, shuffle=True)
test_loader = DataLoader(data_test, batch_size=64, shuffle=True)
for i, data in enumerate(train_loader):
images, labels = data
print(images.shape)
print(labels.shape)
break
for i, data in enumerate(test_loader):
images, labels = data
print(images.shape)
print(labels.shape)
break
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(1, 9, 3)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(9, 5, 3)
self.relu = nn.ReLU()
self.fc1 = nn.Linear(27 * 27 * 5, 1200)
self.fc2 = nn.Linear(1200, 64)
self.fc3 = nn.Linear(64, 2)
def forward(self, x):
outputs = []
x = self.conv1(x)
outputs.append(x)
x = self.relu(x)
outputs.append(x)
x = self.pool(x)
outputs.append(x)
x = self.conv2(x)
x = self.relu(x)
x = self.pool(x)
x = x.view(-1, 27 * 27 * 5)
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.fc3(x)
return outputs
# ----------------------------------------------------可视化卷积核---------------------------------------------------------
# 读取模型
model = torch.load('model.pth')
print(model)
x = images[0].unsqueeze(0)
# forward正向传播过程
out_put = model(x)
weights_keys = model.state_dict().keys()
for key in weights_keys:
print("key :", key)
# 卷积核通道排列顺序 [kernel_number, kernel_channel, kernel_height, kernel_width]
if key == "conv1.weight":
weight_t = model.state_dict()[key].numpy()
print("weight_t.shape", weight_t.shape)
k = weight_t[:, 0, :, :] # 获取第一个卷积核的信息参数
# show 9 kernel ,1 channel
plt.figure()
for i in range(9):
ax = plt.subplot(3, 3, i + 1) # 参数意义:3:图片绘制行数,5:绘制图片列数,i+1:图的索引
plt.imshow(k[i, :, :], cmap='gray')
title_name = 'kernel' + str(i) + ',channel1'
plt.title(title_name)
plt.show()
if key == "conv2.weight":
weight_t = model.state_dict()[key].numpy()
print("weight_t.shape", weight_t.shape)
k = weight_t[:, :, :, :] # 获取第一个卷积核的信息参数
print(k.shape)
print(k)
plt.figure()
for c in range(9):
channel = k[:, c, :, :]
for i in range(5):
ax = plt.subplot(2, 3, i + 1) # 参数意义:3:图片绘制行数,5:绘制图片列数,i+1:图的索引
plt.imshow(channel[i, :, :], cmap='gray')
title_name = 'kernel' + str(i) + ',channel' + str(c)
plt.title(title_name)
plt.show()
得到的结果如下,由于每个卷积层都有很多通道,而一个通道都是一个卷积核。kernel0-8对应着第一个卷积层,而kernel0-4则对应着第二个卷积层。下面的可视化结果分别输出了kernel0-8的第一个通道特征图 和kernel0-4的八个通道(0-7 channels)的特征图。










可以看到,卷积核可视化后,得到的结果非常抽象,但是这些卷积核就是对于特征提取有自己的作用,背后的原理解释可能只有卷积核自己知道。
探索低级特征、中级特征、高级特征
在前面我们介绍了关于低级特征、中级特征与高级特征的概念,现在我们就通过对一张图片的卷积操作,输出其中得到的三种特征进行观察分析。我们这里使用一张细节比较丰富的图片,来观察三种特征的不同。

代码
import matplotlib.pyplot as plt
import torch
from PIL import Image
import numpy as np
import torch.nn as nn
def conv2d(input, Kernel):
plt.figure()
# 创建卷积层,输入通道为1,输出通道为1,卷积核大小为3*3
Conv = nn.Conv2d(1, 1, 3, bias=False)
Conv.weight = torch.nn.Parameter(Kernel)
Conv_output = Conv(input)
Conv_output_picture = Conv_output.squeeze().cpu().detach().numpy()
plt.imshow(Conv_output_picture, cmap='gray')
plt.show()
return Conv_output
# 创建3*3卷积核
Kernel = torch.tensor([[1.0, -1.0, -1.0],
[2.0, 1.0, -2.0],
[1.0, -1.0, -1.0]], dtype=torch.float32).view([1, 1, 3, 3])
# 读取图片
path = r'1.jpg' # 读取图片路径
img = Image.open(path).convert('L') # 转为灰度图
print(img.size)
# 将图像转换为NumPy数组
img = img.resize((300, 300))
img_array = np.array(img)
x = torch.tensor(img_array, dtype=torch.float32).view([1, 1, 300, 300])
x_1 = conv2d(x, Kernel)
x_2 = conv2d(x_1, Kernel)
x_3 = conv2d(x_2, Kernel)
得到的低级特征图:

得到的中级特征图:

得到的高级特征图:

可以看到特征越高级得到的结果越抽象,但是相应的语义也最高级
第一张图展现的是低级特征,主要是一些边缘与轮廓特征,图像中可以看到,边缘还是比较明显的。
第二张图展现的是中级特征,是低级特征的抽象组合,表现为纹理特征等。
第三张图为中级特征的再次抽象组合,语义达到最高,图像已经十分抽象了,还能隐隐约约看到之前的构图。
为什么得到的特征越高级,特征图就越抽象呢?
-
逐层组合特征:在浅层卷积层中,滤波器通常只能检测到简单的边缘或颜色等基本信息。随着层数加深,卷积核会利用之前层中得到的边缘和纹理信息,形成更复杂的结构,如形状和轮廓。
-
空间信息的逐渐缩小:随着网络加深,通常会使用池化层来减少空间分辨率。这种缩小会减少特征图中的细节信息,使网络关注更大的图像区域,从而更容易得到全局性特征,抽象出较高层次的语义信息。
-
高层特征的语义表达:随着卷积网络层数增加,特征图的每个通道包含的信息更加语义化。对于图像分类任务,高级卷积层会专注于目标类别的整体特征,比如识别人脸、物体形状等全局特征,而不再关注边缘和局部的细微特征。
三、总结与心得
1.这一次作业主要是认识了在卷积神经网络过程中的一些新的专业词汇,经过这次作业的学习,我对卷积提取特征这一过程算是彻底明明白白了。
首先,一般在卷积的过程中,会采用局部感知+权值共享的方式来在降低网络复杂度的同时保持网络性能或增加性能,局部感知让每个卷积核专注于特定的特征提取任务,权值共享让这些特征提取任务在整个图像上保持一致。
池化同样是一种降低网络复杂度同时又保持网络性能的手段,通过使用某些值来代表一个区域中的所有像素点,来降低特征尺寸,同时还能很好的继承共有的特征。
同时,通过控制通道大小这一方式来调整卷积过程的特征分布也能够在一定程度上降低网络复杂度,但是通道这个参数不同于前面两种技术,它是需要我们进行相应的参数调整的,所以怎么在模型复杂度与运行速度之间找到平衡就是我们的工作了。
而1x1卷积核在多维卷积中,主要是用于调整特征图的通道数的,其可以保留原有特征图的宽度与高度,同时将通道数改变为与自身相同的通道数。在实际网络应用中,合理利用1x1卷积核可以提升模型的性能。
2.通过本次作业的复现案例,我对于卷积神经网络中的conv2d,MaxPool2d与全连接层的参数计算有了深刻的认识,在以后自己实现网络时会更加熟练。
3.本次作业中,遇到值得产生问题的地方,我都会在博客相应位置附上查阅资料后得到的答案,在数据集加载、展平层计算方式、卷积结果取整等问题上有了一些深刻的研究,贴在这里以后自己忘了也方便进行复习。
4.本次作业不同于上次作业,上次作业更加侧重与卷积核的实现,这次作业更加侧重卷积操作在网络模型中的表现。在自己设计网络结构的时候也是切实体会了一下设计过程中各个部分的作用。
5.通过对特征图卷积核以及初级、中级、高级特征的可视化,我对卷积特征提取这一过程有了更深的理解,不过有些特征图和卷积核确实是很抽象,没有可解释性,但最后得到的效果还是很好的。
参考
【2】四、全卷积网络FCN详细讲解(超级详细哦)_fcn模型-CSDN博客
【3】NNDL 作业七 基于CNN的XO识别_cnn 可视化低级特征 中级特征 高级特征-CSDN博客
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)