[23-24 ]NNDL作业7-基于CNN的XO识别

目录

一、用自己的语言解释以下概念

>局部感知、权值共享

>池化(子采样、降采样、汇聚)。会带来那些好处和坏处?

拓展:空洞卷积

>全卷积网络

>低级特征、中级特征、高级特征

>多通道。N输入,M输出是如何实现的?

>1×1的卷积核有什么作用

二、使用CNN进行XO识别

1.复现参考资料中的代码

*关于扁平化操作Flatten,为什么要给出27*27*5的设定呢?

*跟随老师挂出的博客链接,我查看了关于'  _, ' 的解释:

2.重新设计网络结构

至少增加一个卷积层,卷积层达到三层以上

去掉池化层,对比“有无池化”的效果

修改“通道数”等超参数,观察变化

3.可视化

选择自己的最优模型

可视化部分卷积核和特征图

探索低级特征、中级特征、高级特征 

REF:

三、数据集


一、用自己的语言解释以下概念


>局部感知、权值共享

        局部感知:与全连接神经网络的不同之处是,卷积神经网络的后一层神经元只与前一层的部分神经元连接,只感知局部,而不是整幅图像。这种局部感知的方式使得网络能够专注于图像的局部特征,而不是全局特征。

        表面上看局部连接似乎损失了部分信息,但实际上后层神经元并没有损失信息。通过后面一层神经元感知局部信息不仅可以减少网络需要学习的大量参数,同时可以减少网络的冗余信息。在图像领域,如果网络输入的是一张图片,每个神经元在卷积层都进行局部感知图像信息,经过几层卷积和池化后再通过卷积可以将这些局部的信息进行综合起来得到图像的全局信息。通过局部感知不仅减少了神经网络的复杂性和参数量,同时减少了对训练时设备算力的高要求。

        权重共享:权重共享是指不同的图像或者同一张图像共用一个卷积核,减少重复的卷积核。这种方法能够实现不变性,即无论图像中的模式出现在哪个位置,都可以检测到相同的模式。 在网络对输入图片进行卷积时,对于同一特征的提取,卷积核的参数是共享的,即卷积核中的参数是相同的。这种特性大大减少了卷积神经网络中需要学习的参数

        给一张输入图片,用一个卷积核去扫这张图,卷积核里面的数就叫权重,这张图每个位置是被同样的卷积核扫的,所以权重是一样的,也就是共享。如下图所示:


>池化(子采样、降采样、汇聚)。会带来那些好处和坏处?

        池化(Pooling):作用是进行特征选择,降低特征数量,从而减少参数数量。由于汇聚/池化之后特征图会变得更小,如果后面连接的是全连接层,可以有效地减小神经元的个数,节省存储空间并提高计算效率

        当像素在邻域发生微小位移时,Pooling Layer 的输出是不变的。这就使网络的鲁棒性增强了,有一定抗扰动的作用。常用的汇聚方法有两种,分别是:平均汇聚最大汇聚

池化的好处

  1. 降低特征图的维度:通过池化操作,可以在减小特征图的维度的同时保留重要的特征信息,从而降低计算量并提高模型的效率
  2. 提取主要特征:池化操作可以提取输入特征图中的主要特征,忽略次要特征,进一步提高模型的泛化能力。
  3. 增强模型的不变性:池化操作可以增强模型的不变性,使得模型对于输入图像的旋转、平移等变换具有更好的鲁棒性。

池化的坏处

  1. 信息丢失:池化操作可能导致一些重要的特征信息丢失,这可能会影响模型的性能。
  2. 参数增加:池化操作需要额外的参数来确定池化核的大小和步长等参数,这可能会导致模型参数的增加。
  3. 计算复杂度增加:池化操作需要额外的计算步骤,这可能会导致计算复杂度的增加。

拓展:空洞卷积

        空洞卷积是通过在卷积核中引入空洞来实现的。空洞是指卷积核中的空隙或缺口,卷积核在卷积时不再是像普通卷积那样密集地扫描输入特征图的每个像素点,而是在特定的间隔处进行扫描。这可以扩大卷积核的感受野,从而增加卷积层的感知范围

        空洞卷积在处理高分辨率图像或输入特征图时特别有用,可以避免卷积层输出的特征图尺寸过小而导致信息损失。使用空洞卷积时,需要指定空洞的大小和数量。空洞的大小决定了卷积核在特征图上扫描的距离,而空洞的数量则影响卷积层的感知范围。通常情况下,空洞卷积的空洞大小为1或2,空洞数量为2的整数次幂。

为了更直观的理解,我直接贴上老师分享的图片:

空洞卷积的过程示意如图。

那么空洞卷积有什么作用呢?(来自知乎文章)

>扩大感受野:在deep net中为了增加感受野且降低计算量,总要进行降采样(pooling或s2/conv),这样虽然可以增加感受野,但空间分辨率降低了。为了能不丢失分辨率,且仍然扩大感受野,可以使用空洞卷积。这在检测,分割任务中十分有用。一方面感受野大了可以检测分割大目标,另一方面分辨率高了可以精确定位目标。

>捕获多尺度上下文信息:空洞卷积有一个参数可以设置dilation rate,具体含义就是在卷积核中填充dilation rate-1个0,因此,当设置不同dilation rate时,感受野就会不一样,也即获取了多尺度信息。



>全卷积网络

FCN(全卷积网络)将传统CNN后面的全连接层换成了卷积层,这样网络的输出将是热力图而非类别;见下图(来自知乎文章,博客后方有链接):

第一行,CNN网络在卷积层之后会接上若干个全连接层, 将卷积层产生的特征图(feature map)映射成一个固定长度的特征向量。以AlexNet为代表的经典CNN结构适合于图像级的分类和回归任务,因为它们最后都期望得到整个输入图像的一个数值描述(概率)。

而第二行展示了全连接卷积网络产生的热力图。FCN可以接受任意尺寸的输入图像,采用反卷积层对最后一个卷积层的feature map进行上采样, 使它恢复到输入图像相同的尺寸,从而可以对每个像素都产生了一个预测, 同时保留了原始输入图像中的空间信息, 最后在上采样的特征图上进行逐像素分类。



>低级特征、中级特征、高级特征

低级特征通常包含图像的底层信息,例如颜色、纹理等。这些特征通常是最基本的视觉信息,可以用于图像的初步分析和识别。

中级特征通常包括对象信息以及形状和空间信息等。这些特征将低级特征组合起来,形成更具有代表性和抽象性的特征表示。它们可以提供关于对象属性和相互关系的更多信息,有助于更深入地理解图像内容。

高级特征通常包含对象内在的语义信息。这些特征通过对中级特征的进一步组合和整合,能够对整体图像的语义信息和高层次结构进行建模。它们可提供关于图像中对象和场景的更高级别的理解。

以下解释来自知乎(文章末尾有链接),更加明确:

浅层网络感受野较小,更加注重细节信息;深层网络感受野较大,更加注重全局信息

在进行浅层特征提取时,能够利用更多的细粒度特征信息,而且此时特征图每个像素点对应的感受野重叠区域还很小,这就保证了网络能够捕获更多细节。浅层特征具有更高的分辨率。

在进行深层特征提取时,随着下采样或卷积次数增加,感受野逐渐增加,感受野之间重叠区域也不断增加,此时的像素点代表的信息是一个区域的信息,获得的是这块区域或相邻区域之间的特征信息,相对不够细粒度、分辨率较低,但语义信息丰富。

(就像用手电筒照一堵墙,光源离墙越远,光圈能覆盖的范围越大,但亮度也会降低)



>多通道。N输入,M输出是如何实现的?

老师分享的PPT中的图片过程十分直观、简明又易懂:

如上图展示了5*5*3channels 输入特征图和 3*3*3channels的卷积核进行卷积, 其中每个通道(channels)都会得到一个形状为3*3卷积结果, 将3个卷积后的结果按位置相加,最终得到了3*3*1channels的特征图。

这2张图更加直观的展示了多通道输入在经过卷积操作后(卷积核的通道数需和输入通道数保持一致),得到了通道数为1的特征图。

所以到目前为止,我们已经明白了N输入,1输出的原理了。那么如何实现N输入,M输出呢?

对上方的图例进行一些扩展:

在N通道,1输出的图例当中,只有一组卷积核,每组卷积核的通道数为3。而在这副图例中,存在了m组卷积核(filter 也成为滤波器),m组卷积核对输入特征进行卷积,得到了m组Feature map,最后将得到的m组Featuer map整合为新的特征结果,其通道数为m。

下图展示的更直观,自行体会,理清m和n:



>1×1的卷积核有什么作用

1*1只改变输入的通道数channels,不改变输入的形状。1*1卷积核可以起到升维和降维的作用。

以下是知乎回答中我能理解的解释,并且附上一张图:

  • 调节通道数
            由于 1*1卷积核并不会改变 height 和 width,改变通道的第一个最直观的结果,就是可以将原本的数据量进行增加或者减少。这里看其他文章或者博客中都称之为升维、降维。但实际情况维度并没有改变,改变的只是 height×width×channels中的channels这一个维度的大小而已。
  • 增加非线性
           可在保持特征图尺度不变的的前提下大幅增加非线性特性(利用后接的非线性激活函数如ReLU)。非线性允许网络学习更复杂的功能,并且使得整个网络能够进一步加深。
  • 减少参数
           前面所说的降维,其实也是减少了参数,因为特征图少了,参数也自然跟着就减少,相当于在特征图的通道数上进行卷积,压缩特征图,二次提取特征,使得新特征图的特征表达更佳。

        橙色长方体代表卷积核,尺寸为1*1,而它的通道数channels与输入卷积的通道数保持一致,最后输出的卷积特征图通道为1(厚度为1)。看下图,当进行多通道卷积时,最终的输出要将每个通道上得到的卷积结果进行累加。    而1*1卷积核的作用可以在特征图的长与宽不变的前提下,降低维度(或者升维)。

(40 封私信 / 80 条消息) 卷积神经网络中的1*1卷积究竟有什么用? - 知乎 (zhihu.com)

一文读懂卷积神经网络中的1x1卷积核 - 知乎 (zhihu.com)



二、使用CNN进行XO识别


1.复现参考资料中的代码

加载、处理图像数据集,并实现可视化

代码:

#数据集
from torch.utils.data import DataLoader
from torchvision import transforms, datasets

transforms = transforms.Compose([
    transforms.ToTensor(),  # 把图片进行归一化,并把数据转换成Tensor类型
    transforms.Grayscale(1)  # 把图片 转为灰度图
])

data_train = datasets.ImageFolder('train_data', transforms)
data_test = datasets.ImageFolder('test_data', 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

#可视化数据集
import matplotlib.pyplot as plt
a=0
plt.figure()
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()
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()

结果:

这张结果显示了训练集和测试集中图像的shape属性,其中的torch.size([64,1,116,116])的四个维度分别表示:

第一维:64 - 表示批量大小(batch size)即图像的数量,此时模型将处理64张图像。

第二维:1 - 表示通道数。对于彩色图像,通道数为3(红绿蓝); 对于灰度图像通道数为1。

第三维、第四维:116, 116 - 表示图像的高度和宽度。

可视化:


构建模型  

使用Conv2d方法创建2D卷积层,设置输出、输入通道大小与卷积核形状大小。在卷积层后接全连接层。进行前向传播,前向传播的步骤为 [卷积-激活-池化]-扁平化-全连接层。 代码如下,注释和函数解释都已写清:

import torch.nn as nn

class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        #这是2D卷积层,输入通道为1,输出通道为9. 卷积核大小为3
        self.conv1 = nn.Conv2d(1, 9, 3)

        #这是一个2D最大池化层,使用2*2的窗口进行最大值操作,将卷积层的输出进行池化。
        self.maxpool = nn.MaxPool2d(2, 2)

        #卷积层2.输入通道为9(上一个卷积层的输出通道数),输出通道为5,卷积核大小为3
        self.conv2 = nn.Conv2d(9, 5, 3)

        # 非线性激活函数。可以将负值变为0,正直保持不变
        self.relu = nn.ReLU()

        #这是全连接层,输入节点数为27*27*5,输出节点数为1200
        self.fc1 = nn.Linear(27 * 27 * 5, 1200)
        self.fc2 = nn.Linear(1200, 64)#全连接层2 输入节点数为上一个连接层输出的节点数
        self.fc3 = nn.Linear(64, 2)

    #前向传播
    def forward(self, x):
        #x经过2个卷积层和2个全连接层 返回输出结果x
        x = self.maxpool(self.relu(self.conv1(x)))#卷积-ReLu激活-最大池化
        x = self.maxpool(self.relu(self.conv2(x)))#卷积-ReLu激活-最大池化

        #Flatten 扁平化处理
        x = x.view(-1, 27 * 27 * 5) #修改x的形状,返回新的视图,不会改变原有的数据

        #全连接层
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        x = self.fc3(x) #最后一层不激活
        return x

再上一张祖传图,更加直观清晰的了解卷积过程,其中的flatten为扁平化操作,为了使数据的形状能够衔接卷积结果与全连接层

*关于扁平化操作Flatten,为什么要给出27*27*5的设定呢?

首先需要了解输出特征图的尺寸如何计算?简明易懂——卷积神经网络的输入输出特征图大小计算_输出特征图的大小怎么算-CSDN博客

上面这篇博客里描述得很清楚,我直接给出计算公式:

假设特征图的尺寸为M*M, 卷积核尺寸为N*N, 步长为S,填充为P,则:

M_{out}=\frac{M_{in}+2P-N}{S}+1

即为:  输出尺寸 = (输入尺寸  + 2*填充尺寸-卷积核尺寸 )/ 步长 + 1


训练模型

代码:

import torch
from torchvision import transforms, datasets
import torch.nn as nn
from torch.utils.data import DataLoader
import torch.optim as optim

transforms = transforms.Compose([
    transforms.ToTensor(),  # 把图片进行归一化,并把数据转换成Tensor类型
    transforms.Grayscale(1)  # 把图片 转为灰度图
])

path = r'train_data'
path_test = r'test_data'

data_train = datasets.ImageFolder(path, transform=transforms)
data_test = datasets.ImageFolder(path_test, transform=transforms)

print("size of train_data:", len(data_train))
print("size of test_data:", len(data_test))

data_loader = DataLoader(data_train, batch_size=64, shuffle=True)
data_loader_test = DataLoader(data_test, batch_size=64, shuffle=True)

for i, data in enumerate(data_loader):
    images, labels = data
    print(images.shape)
    print(labels.shape)
    break

for i, data in enumerate(data_loader_test):
    images, labels = data
    print(images.shape)
    print(labels.shape)
    break

class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        #这是2D卷积层,输入通道为1,输出通道为9. 卷积核大小为3
        self.conv1 = nn.Conv2d(1, 9, 3)

        #这是一个2D最大池化层,使用2*2的窗口进行最大值操作,将卷积层的输出进行池化。
        self.maxpool = nn.MaxPool2d(2, 2)

        #卷积层2.输入通道为9(上一个卷积层的输出通道数),输出通道为5,卷积核大小为3
        self.conv2 = nn.Conv2d(9, 5, 3)

        # 非线性激活函数。可以将负值变为0,正直保持不变
        self.relu = nn.ReLU()

        #这是全连接层,输入节点数为27*27*5,输出节点数为1200
        self.fc1 = nn.Linear(27 * 27 * 5, 1200)
        self.fc2 = nn.Linear(1200, 64)#全连接层2 输入节点数为上一个连接层输出的节点数
        self.fc3 = nn.Linear(64, 2)

    #前向传播
    def forward(self, x):
        #x经过2个卷积层和2个全连接层 返回输出结果x
        x = self.maxpool(self.relu(self.conv1(x)))#卷积-ReLu激活-最大池化
        x = self.maxpool(self.relu(self.conv2(x)))#卷积-ReLu激活-最大池化

        #Flatten 扁平化处理
        x = x.view(-1, 27 * 27 * 5) #修改x的形状,返回新的视图,不会改变原有的数据

        #全连接层
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        x = self.fc3(x) #最后一层不激活
        return x

#实例化模型
model = Net()

criterion = torch.nn.CrossEntropyLoss()  # 损失函数 交叉熵损失函数
optimizer = optim.SGD(model.parameters(), lr=0.1)  #优化函数:随机梯度下降,学习率为0.1

epochs = 10
for epoch in range(epochs):
    running_loss = 0.0
    for i, data in enumerate(data_loader):
        images, label = data
        out = model(images)
        loss = criterion(out, label)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        if (i + 1) % 10 == 0:
            print('[%d  %5d]   loss: %.3f' % (epoch + 1, i + 1, running_loss / 100))
            running_loss = 0.0

print('finished train')

# 保存模型 torch.save(model.state_dict(), model_path)
torch.save(model.state_dict(), 'model_name1.pth')  # 保存的是模型, 不止是w和b权重值

# 读取模型
model = torch.load('model_name1.pth')

训练结果为:


模型测试/计算模型准确率

在测试模式下,不更新网络梯度,并计算网络在测试数据集上的准确性。对于每个测试样本,模型会输出预测结果,然后计算预测结果和真实标签之间的匹配程度。代码如下:

# https://blog.csdn.net/qq_53345829/article/details/124308515
import torch
from torchvision import transforms, datasets
import torch.nn as nn
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
import torch.optim as optim
 
transforms = transforms.Compose([
    transforms.ToTensor(),  # 把图片进行归一化,并把数据转换成Tensor类型
    transforms.Grayscale(1)  # 把图片 转为灰度图
])
 
path = r'train_data'
path_test = r'test_data'
 
data_train = datasets.ImageFolder(path, transform=transforms)
data_test = datasets.ImageFolder(path_test, transform=transforms)
 
print("size of train_data:", len(data_train))
print("size of test_data:", len(data_test))
 
data_loader = DataLoader(data_train, batch_size=64, shuffle=True)
data_loader_test = DataLoader(data_test, batch_size=64, shuffle=True)

#打印数据集大小
print(len(data_loader))
print(len(data_loader_test))
 
 
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 9, 3)  # in_channel , out_channel , kennel_size , stride
        self.maxpool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(9, 5, 3)  # in_channel , out_channel , kennel_size , stride
 
        self.relu = nn.ReLU()
        self.fc1 = nn.Linear(27 * 27 * 5, 1200)  # full connect 1
        self.fc2 = nn.Linear(1200, 64)  # full connect 2
        self.fc3 = nn.Linear(64, 2)  # full connect 3
 
    def forward(self, x):
        x = self.maxpool(self.relu(self.conv1(x)))
        x = self.maxpool(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 = Net()
model.load_state_dict(torch.load('model_name1.pth', map_location='cpu')) # 导入网络的参数
 
# model_load = torch.load('model_name1.pth')
# https://blog.csdn.net/qq_41360787/article/details/104332706
 
correct = 0
total = 0
with torch.no_grad():  # 进行评测的时候网络不更新梯度
    for data in data_loader_test:  # 读取测试集
        images, labels = data
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)  # 取出 最大值的索引 作为 分类结果
        total += labels.size(0)  # labels 的长度
        correct += (predicted == labels).sum().item()  # 预测正确的数目
print('Accuracy of the network on the  test images: %f %%' % (100. * correct / total))
 
#  "_," 的解释 https://blog.csdn.net/weixin_48249563/article/details/111387501

结果:

预测结果和真实结果的匹配程度达到了99.66%,很完美的模型。

*跟随老师挂出的博客链接,我查看了关于'  _, ' 的解释:

原文链接: PyTorch系列 | _, predicted = torch.max(outputs.data, 1)的理解-CSDN博客

简单来讲就是    _, predicted = torch.max(outputs.data, 1) 中的torch.max返回两个值,一个是最大值value,用'_'来接收 ;另一个是value值所在的index索引位置,由predicted来接收。   我们在分类任务中只关心分类的预测类别,而不关心具体的预测概率。所以最大值value就可以使用没有意义的'_'来接收,不必再耗费掉一个变量名了。

原博主讲的特别清晰,想深入了解一些的可以看看原文:


查看训练好的模型特征图

此过程展示了使用PyTorch进行卷积神经网络模型的构建、预训练权重加载、正向传播以及特征图的可视化。 代码详解我都写清楚放在了代码注释中,看得懂代码就能更深层的理解卷积过程和原理了

# 看看每层的 卷积核 长相,特征图 长相
# 获取网络结构的特征矩阵并可视化
import torch
import matplotlib.pyplot as plt
import numpy as np
from torchvision import transforms, datasets
import torch.nn as nn
from torch.utils.data import DataLoader

#  定义图像预处理过程(要与网络模型训练过程中的预处理过程一致)
transforms = transforms.Compose([
    transforms.ToTensor(),  # 把图片进行归一化,并把数据转换成Tensor类型
    transforms.Grayscale(1)  # 把图片 转为灰度图
])

#定义训练数据的路径
path = r'train_data'
#使用ImageFolder从指定路径加载图像数据,对数据进行预处理
data_train = datasets.ImageFolder(path, transform=transforms)
#创建DataLoader,用于批量加载数据,并打乱数据顺序
data_loader = DataLoader(data_train, batch_size=64, shuffle=True)
for i, data in enumerate(data_loader):
    images, labels = data
    print('images.shape:',images.shape)
    print('labels.shape',labels.shape)
    break


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 9, 3)  # in_channel , out_channel , kennel_size , stride
        self.maxpool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(9, 5, 3)  # in_channel , out_channel , kennel_size , stride

        self.relu = nn.ReLU()
        self.fc1 = nn.Linear(27 * 27 * 5, 1200)  # full connect 1
        self.fc2 = nn.Linear(1200, 64)  # full connect 2
        self.fc3 = nn.Linear(64, 2)  # full connect 3

    def forward(self, x):
        outputs = []
        x = self.conv1(x)
        outputs.append(x) #保存经过卷积层1后的输出
        x = self.relu(x)
        outputs.append(x)#保存经过激活的输出
        x = self.maxpool(x)
        outputs.append(x)#保存经过最大池化操作的输出

        x = self.conv2(x)
        x = self.relu(x)
        x = self.maxpool(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


# create model
model1 = Net()

# load model weights加载预训练权重
# model_weight_path ="./AlexNet.pth"
model_weight_path = "model_name1.pth"

#加载预训练的模型权重到model1
model1.load_state_dict(torch.load(model_weight_path))

# 打印出模型的结构
print('模型结构:','\n',model1)

x = images[0] #选取输入的第一张图象
out_put = model1(x) #进行forward正向传播过程,得到输出结果

#特征图可视化
for feature_map in out_put: #遍历模型的输出结果(特征图)
    # [N, C, H, W] -> [C, H, W]    维度变换
    #将pytorch转换为numpy数组,并去除额外的维度
    im = np.squeeze(feature_map.detach().numpy())
    # [C, H, W] -> [H, W, C]
    im = np.transpose(im, [1, 2, 0]) #对特征图进行维度变换
    print('im.shape:','\n',im.shape)

    # show 9 feature maps
    plt.figure()
    for i in range(9):
        ax = plt.subplot(3, 3, i + 1) #i+1:图的索引
        # [H, W, C]
        # 特征矩阵每一个channel对应的是一个二维的特征矩阵,就像灰度图像一样,channel=1
        # plt.imshow(im[:, :, i])
        plt.imshow(im[:, :, i], cmap='gray')
    plt.show()

结果为:

在前向传播的函数当中,我们使用outputs列表存储了每一层操作后的输出结果,并且print出来,这样可以理解卷积神经网络运行的更多原理和细节。

可视化结果,由上至下分别为卷积后-激活后-池化后的3*3视图


查看训练好的模型的卷积核

CNN的卷积核是模型自己训练得到的,不需要人工干预类似于FNN中的权值w,通过反向传播、梯度下降不断更新。

# 看看每层的 卷积核 长相,特征图 长相
# 获取网络结构的特征矩阵并可视化
import torch
import matplotlib.pyplot as plt
import numpy as np
from torchvision import transforms, datasets
import torch.nn as nn
from torch.utils.data import DataLoader

#  定义图像预处理过程(要与网络模型训练过程中的预处理过程一致)
transforms = transforms.Compose([
    transforms.ToTensor(),  # 把图片进行归一化,并把数据转换成Tensor类型
    transforms.Grayscale(1)  # 把图片 转为灰度图
])

#定义训练数据的路径
path = r'train_data'
#使用ImageFolder从指定路径加载图像数据,对数据进行预处理
data_train = datasets.ImageFolder(path, transform=transforms)
#创建DataLoader,用于批量加载数据,并打乱数据顺序
data_loader = DataLoader(data_train, batch_size=64, shuffle=True)
for i, data in enumerate(data_loader):
    images, labels = data
    print('images.shape:',images.shape)
    print('labels.shape',labels.shape)
    break


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 9, 3)  # in_channel , out_channel , kennel_size , stride
        self.maxpool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(9, 5, 3)  # in_channel , out_channel , kennel_size , stride

        self.relu = nn.ReLU()
        self.fc1 = nn.Linear(27 * 27 * 5, 1200)  # full connect 1
        self.fc2 = nn.Linear(1200, 64)  # full connect 2
        self.fc3 = nn.Linear(64, 2)  # full connect 3

    def forward(self, x):
        outputs = []
        x = self.conv1(x)
        outputs.append(x) #保存经过卷积层1后的输出
        x = self.relu(x)
        outputs.append(x)#保存经过激活的输出
        x = self.maxpool(x)
        outputs.append(x)#保存经过最大池化操作的输出

        x = self.conv2(x)
        x = self.relu(x)
        x = self.maxpool(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


# create model
model1 = Net()

# load model weights加载预训练权重
# model_weight_path ="./AlexNet.pth"
model_weight_path = "model_name1.pth"

#加载预训练的模型权重到model1
model1.load_state_dict(torch.load(model_weight_path))

# 打印出模型的结构
print('模型结构:','\n',model1)

x = images[0] #选取输入的第一张图象
out_put = model1(x) #进行forward正向传播过程,得到输出结果

#获取模型所有权重键key
weights_keys = model1.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 = model1.state_dict()[key].numpy()#获取卷积层的权重,并将Tensor转换为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 = model1.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的八个通道(0-7 channels)的特征图。每个卷积核都有8个通道,这样我们就可以直观的看到每个卷积核、每个通道的特征图样貌是什么样的了。

结果为:


2.重新设计网络结构

  • 至少增加一个卷积层,卷积层达到三层以上

根据这个流程设计卷积神经网络层,[卷积-激活-池化]

在本次实验中设步长S和填充P都为0。  由于增加了一层卷积层,所以Flatten展平环节的参数需要再次计算。展平操作后的x的形状应为:

116-3+1  = 114   (卷积第一次)

114/2 = 57   (池化一次  池化层的形状为2*2)

57-3+1 = 55        (卷积第二次)

55/2 = 27.5 向下取整,取27 (池化第二次)

27-3+1 = 25        (卷积第三次)

25/2 = 12.5 向下取整,为12  (池化第三次)

(至于为何要向下取整,可看博客:【精选】NNDL 作业6:基于CNN的XO识别-CSDN博客

注意,不仅要修改卷积结束后输出特征图形状为12的参数,还需要将自定义的输出通道数同压平操作的输入节点数进行同步修改。代码:

import torch
from torchvision import transforms, datasets
import torch.nn as nn
from torch.utils.data import DataLoader
import torch.optim as optim

transforms = transforms.Compose([
    transforms.ToTensor(),  # 把图片进行归一化,并把数据转换成Tensor类型
    transforms.Grayscale(1)  # 把图片 转为灰度图
])

path = r'train_data'
path_test = r'test_data'

data_train = datasets.ImageFolder(path, transform=transforms)
data_test = datasets.ImageFolder(path_test, transform=transforms)

print("size of train_data:", len(data_train))
print("size of test_data:", len(data_test))

data_loader = DataLoader(data_train, batch_size=64, shuffle=True)
data_loader_test = DataLoader(data_test, batch_size=64, shuffle=True)

for i, data in enumerate(data_loader):
    images, labels = data
    print(images.shape)
    print(labels.shape)
    break

for i, data in enumerate(data_loader_test):
    images, labels = data
    print(images.shape)
    print(labels.shape)
    break

class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        #这是2D卷积层,输入通道为1,输出通道为9. 卷积核大小为3
        self.conv1 = nn.Conv2d(1, 9, 3)
        #卷积层2.输入通道为9(上一个卷积层的输出通道数),输出通道为5,卷积核大小为3
        self.conv2 = nn.Conv2d(9, 5, 3)
        #卷积层3.输入通道为5(上一个卷积层的输出通道数),输出通道为4,卷积核大小为3
        self.conv3 = nn.Conv2d(5, 4, 3)

        #这是一个2D最大池化层,使用2*2的窗口进行最大值操作,将卷积层的输出进行池化。
        self.maxpool = nn.MaxPool2d(2, 2)

        # 非线性激活函数。可以将负值变为0,正直保持不变
        self.relu = nn.ReLU()

        #这是全连接层,输入节点数为12*12*4,输出节点数为1200
        self.fc1 = nn.Linear(12 * 12 * 4, 1200)
        self.fc2 = nn.Linear(1200, 64)#全连接层2 输入节点数为上一个连接层输出的节点数
        self.fc3 = nn.Linear(64, 2)

    #前向传播
    def forward(self, x):
        #x经过2个卷积层和2个全连接层 返回输出结果x
        x = self.maxpool(self.relu(self.conv1(x)))#卷积-ReLu激活-最大池化
        x = self.maxpool(self.relu(self.conv2(x)))#卷积-ReLu激活-最大池化
        x = self.maxpool(self.relu(self.conv3(x)))  # 卷积-ReLu激活-最大池化

        #Flatten 扁平化处理
        x = x.view(-1, 12 * 12 * 4) #修改x的形状,返回新的视图,不会改变原有的数据

        #全连接层
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        x = self.fc3(x) #最后一层不激活
        return x

#实例化模型
model = Net()

criterion = torch.nn.CrossEntropyLoss()  # 损失函数 交叉熵损失函数
optimizer = optim.SGD(model.parameters(), lr=0.1)  #优化函数:随机梯度下降,学习率为0.1

epochs = 10
for epoch in range(epochs):
    running_loss = 0.0
    for i, data in enumerate(data_loader):
        images, label = data
        out = model(images)
        loss = criterion(out, label)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        running_loss += loss.item()
        if (i + 1) % 10 == 0:
            print('[%d  %5d]   loss: %.3f' % (epoch + 1, i + 1, running_loss / 100))
            running_loss = 0.0

print('finished train')

训练结果:


  • 去掉池化层,对比“有无池化”的效果

去掉池化层,保持三层卷积层,重新计算flatten展平操作中的参数:
116-3+1 = 114   (卷积第一次)

114-3+1 = 112     (卷积第二次)

112-3+1 = 110     (卷积第三次)

代码和上一问的一样,就是改了改扁平参数,去掉了池化层函数。

训练时由于特征图的尺寸太大,没有经过池化层降低特征图的规模,所以训练时长很大.

训练过程等了将近20分钟~ 结果为:

对比上一问我运行出来的结果,损失值loss并没有下降,没有收敛。这是由于参数量太大,没有池化层降低参数的操作,会使训练模型崩溃,发挥不出作用。

再次复习一下池化层的好处,结合实践,才有更深的感悟:

  1. 降低特征图的维度:通过池化操作,可以在减小特征图的维度的同时保留重要的特征信息,从而降低计算量并提高模型的效率。
  2. 提取主要特征:池化操作可以提取输入特征图中的主要特征,忽略次要特征,进一步提高模型的泛化能力。
  3. 增强模型的不变性:池化操作可以增强模型的不变性,使得模型对于输入图像的旋转、平移等变换具有更好的鲁棒性。

  • 修改“通道数”等超参数,观察变化

在三层卷积层+有池化层的基础上修改通道数channles的值,观察训练结果变化:

我随机设置一些通道数值:

Conv1的输入通道为1,输出通道为8

Conv2的输入通道为8,输出通道为4

Conv1的输入通道为4,输出通道为4

重新计算Flatten扁平操作中的参数:

116-3+1  = 114   (卷积第一次)

114/2 = 57   (池化一次  池化层的形状为2*2)

57-3+1 = 55        (卷积第二次)

55/2 = 27.5 向下取整,取27 (池化第二次)

27-3+1 = 25        (卷积第三次)

25/2 = 12.5 向下取整,为12  (池化第三次)

发现通道数的改变并不影响Flatten扁平化参数值的改变。

进行模型训练,查看结果:

对比最初模型的训练效果(下图),我修改的这个模型的loss值没有收敛,没有下降,效果不好。起初没有想过原来通道数的改变也会影响模型的性能,原来在这里面还另有一番研究。

*那么通道数对模型的性能有什么影响?

  1. 增加模型的表征能力:通过增加卷积核通道数,可以增加模型的表征能力,提高模型的性能。通道数的增加会扩大卷积核在特征空间的搜索范围,从而能够更好地捕捉到数据的局部特征。
  2. 优化卷积操作:卷积操作是一种非常计算密集的操作,通道数的增加可以优化卷积操作的效率。当卷积核通道数较多时,可以利用硬件并行化的特性,使卷积操作更加高效。
  3. 降低过拟合风险:如果卷积核通道数过小,那么卷积输出的特征可能无法包含数据的全部信息,导致模型出现欠拟合。而如果卷积核通道数过多,模型可能会过拟合。因此,卷积核通道数的选择需要在欠拟合和过拟合之间找到一个平衡点,从而达到最优的性能。

在实践中,通道数的选择应根据具体问题和需求来确定。如果通道数过小,可能会导致欠拟合和表征能力的不足;如果通道数过大,可能会导致过拟合和计算效率的下降。通常会从较小的通道数开始尝试,然后逐渐增加通道数,直到达到满足要求的效果为止。

想要更深入地了解channles的设置对卷积神经网络模型有何影响,可以去看篇知乎文章,大佬总结的非常好,很有权威性:(41 封私信 / 80 条消息) 在卷积神经网络中,channels作用是什么,channels数量越多是否提取上下文能力越强? - 知乎 (zhihu.com)


3.可视化

  • 选择自己的最优模型

经过上一阶段的修改参数实验可知,若想达到卷积效果最优,我们不仅要适当的增加网络的深度(网络的卷积层层数),还要选取合适的网络宽度(卷积层、卷积核的channels通道数)。

经过多次 反复的调参,发现99.6%准确率的模型是我调试许多参数后能达到最高的准确率了:

import torch
from torchvision import transforms, datasets
import torch.nn as nn
from torch.utils.data import DataLoader

transforms = transforms.Compose([
    transforms.ToTensor(),  # 把图片进行归一化,并把数据转换成Tensor类型
    transforms.Grayscale(1)  # 把图片 转为灰度图
])

path = r'train_data'
path_test = r'test_data'

data_train = datasets.ImageFolder(path, transform=transforms)
data_test = datasets.ImageFolder(path_test, transform=transforms)

print("size of train_data:", len(data_train))
print("size of test_data:", len(data_test))

data_loader = DataLoader(data_train, batch_size=64, shuffle=True)
data_loader_test = DataLoader(data_test, batch_size=64, shuffle=True)

# 打印数据集大小
print(len(data_loader))
print(len(data_loader_test))


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 9, 3)  # in_channel , out_channel , kennel_size , stride
        self.maxpool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(9, 5, 3)  # in_channel , out_channel , kennel_size , stride

        self.relu = nn.ReLU()
        self.fc1 = nn.Linear(27 * 27 * 5, 1200)  # full connect 1
        self.fc2 = nn.Linear(1200, 64)  # full connect 2
        self.fc3 = nn.Linear(64, 2)  # full connect 3

    def forward(self, x):
        x = self.maxpool(self.relu(self.conv1(x)))
        x = self.maxpool(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 = Net()
model.load_state_dict(torch.load('model_name1.pth', map_location='cpu'))  # 导入网络的参数

correct = 0
total = 0
with torch.no_grad():  # 进行评测的时候网络不更新梯度
    for data in data_loader_test:  # 读取测试集
        images, labels = data
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)  # 取出 最大值的索引 作为 分类结果
        total += labels.size(0)  # labels 的长度
        correct += (predicted == labels).sum().item()  # 预测正确的数目
print('Accuracy of the network on the  test images: %f %%' % (100. * correct / total))

  • 可视化部分卷积核和特征图

     在上一阶段我们已经进行过可视化卷积核与特征图,请回看。


  • 探索低级特征、中级特征、高级特征 

在开头的概念解释中,我们解释了低级特征、中级特征与高级特征的概念和特点。现在我们通过卷积神经网络模型代码进行实现,展现一下低级、中级、高级特征的具体形态。接下来,我们对这张图片进行卷积(河北大学校园一角):
 

代码思路来源于同班大佬果同学(文末有链接),根据这个思路,我又手敲了一遍。

创建一个卷积层和卷积核,连续卷积三次,分别得到了低级、中级和高级特征结果:

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'C:\Users\27513\Pictures\Camera Roll\hbu1.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)

低级特征:

中级特征:


高级特征:

可以看到图像中建筑物的轮廓都由清晰逐渐抽象,这体现了卷积过程在不断的加深,所提取出来的特征也更趋向于抽象化,而非具象化。

根据所得到的卷积效果,我们再来回顾一下低级、中级、高级特征的特性:

网络中靠前的部分提取的是初级特征,例如边缘特征。

网络中段提取的是中级特征,中级特征一定程度上是初级特征的再组合体现,比如纹理特征等。

网络后端提取的是高级特征,高级特征可以看作中级特征的再组合,也更加抽象。应用分层特征提取的思想,可以提升网络对图片特征的敏感程度。


REF:

卷积神经网路 Convolutional Neural Networks · 資料科學・機器・人 (mcknote.com)


三、数据集

老师发到了QQ学习群里,共2000张图片,1000张'O',1000张'X'。

要分为train训练集和test测试集,分别各取150张O和X作为测试集,其余的1700(850张'O' 850张'X')张图片作为训练集。  导入到pycharm项目列表中。(需要数据集的可以在评论区找我要哈)


本博客中借鉴到的内容如下,在此鸣谢:

总结-空洞卷积(Dilated/Atrous Convolution) - 知乎 (zhihu.com)

分层特征提取Hierarchical Feature Extraction (baidu.com)

DL Homework 7-CSDN博客

(41 封私信 / 80 条消息) 在卷积神经网络中,channels作用是什么,channels数量越多是否提取上下文能力越强? - 知乎 (zhihu.com)

简明易懂——卷积神经网络的输入输出特征图大小计算_输出特征图的大小怎么算-CSDN博客

(40 封私信 / 80 条消息) 卷积神经网络中的1*1卷积究竟有什么用? - 知乎 (zhihu.com)

四、全卷积网络FCN详细讲解(超级详细哦)_fcn模型-CSDN博客

一文读懂卷积神经网络中的1x1卷积核 - 知乎 (zhihu.com)

低级特征与高级特征、全局特征与局部特征、浅层特征与深层特征 - 知乎 (zhihu.com)

Pooling(池化)的好处以及CNN优缺点_池化技术的优缺点-CSDN博客

【精选】NNDL 作业6:基于CNN的XO识别-CSDN博客

【23-24 秋学期】NNDL 作业7 基于CNN的XO识别-CSDN博客
局部感知与权值共享_局部感受野和权值共享_Le0v1n的博客-CSDN博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

洛杉矶县牛肉板面

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值