基于CNN的XO识别
一、用自己的语言解释以下概念
1.局部连接、权重共享
局部连接
局部连接是指后一层的每个神经元只与前一层中的部分神经元相关。
为什么可以使用局部连接?局部连接不会丢失信息吗?
一般认为图像局部的像素联系比较密切,而距离较远的像素相关性较弱,因此,每个神经元没必要对全局图像进行感知,只要对局部进行感知,然后在更高层将局部的信息综合起来得到全局信息。
在图像领域,如果网络输入的是一张图片,每个神经元在卷积层都进行局部感知图像信息,经过几层卷积和池化后再通过卷积可以将这些局部的信息进行综合起来得到图像的全局信息。使得CNN在不同层次上逐渐提取和组合不同抽象级别的特征。
权重共享
权重共享是指当我们想提取某一种特征时,扫描整幅图所有的部分用的是同一个卷积核。因为一个卷积核可以提取一种特征,如果我们想提取多种特征,需要使用多种卷积核。
局部连接和权重共享的优势
1)平移不变性: 通过局部连接,CNN能够捕捉到输入数据中的局部特征,而不受其在图像或其他数据中的具体位置的影响。这种位置不变性使得CNN对于物体识别、目标检测等任务具有鲁棒性,即使目标出现在不同位置,网络仍然能够正确地识别它们
。
2)稀疏交互: 通过卷积操作,CNN实现了对输入数据的稀疏交互。每个输出值只依赖于输入数据中的少量局部区域。这种稀疏交互使得CNN能够更有效地利用计算资源,同时减少了对不相关信息的处理,提高了网络的计算效率
。
3)减少参数数量: 使用同一卷积核对输入图像进行卷积,使网络中需要学习的参数数量大大减少,从而减轻了网络的复杂性,降低了过拟合的风险。
卷积层参数/卷积计算量
卷积参数 = 卷积核长度x卷积核宽度x输入通道数x输出通道数+输出通道数(偏置)
卷积计算量 = 输出数据大小x卷积核的尺度x输入通道数
例:输入:224x224x3,输出:224x244x64,卷积核:3x3
参数量 = 3x3x3x64+64
计算量 = 224x224x64x3x3x3
2.池化(子采样、降采样、汇聚)。会带来那些好处和坏处?
池化操作通过滑动窗口在输入特征图上移动,并从每个窗口中提取一个值。这个值通常是窗口内元素的最大值(最大池化)或平均值(平均池化)。常见的池化方式有最大池化和平均池化。
池化的好处:
1)降低计算量和参数数量: 池化通过减小特征图的空间尺寸,可以减少后续层需要处理的参数数量,从而降低了整个网络的计算复杂度。
2)增加局部平移不变性: 池化操作有助于提高网络对输入数据的平移不变性,因为池化操作会选择窗口内的最大值或平均值作为汇聚值,无论这些值出现在输入特征图的哪个位置。
3)提取显著特征: 最大池化能够帮助网络识别出输入数据中的显著特征,因为它选择窗口内的最大值作为汇聚值,从而将重要的特征信息保留下来。
4)减少过拟合风险: 池化操作通过降低特征图的细节信息,有助于减少网络的过拟合风险,同时提高网络的泛化能力。
池化的坏处:
1)信息丢失: 池化操作可能导致信息丢失,特别是对于较小的目标或细节信息。在某些情况下,池化操作可能会丢失一些关键的细节,从而影响网络对输入数据的理解和判断能力。
2)固定的聚合方式: 池化操作采用固定的聚合方式(如最大值或平均值),这种固定的方式可能不适用于所有类型的数据和任务。有时候,更灵活的聚合方式可能会更适合某些特定的应用场景。
3)降低空间分辨率: 池化操作会减小特征图的空间尺寸,这在一定程度上降低了空间分辨率。对于某些需要精细空间信息的任务,如目标检测中的小目标识别,池化可能会带来一定的负面影响。
3.全卷积网络(课上讲的这个概念不准确,同学们查资料纠正一下)
定义: FCN将传统CNN后面的全连接层换成了卷积层,这样网络的输出将是热力图而非类别;同时,为解决卷积和池化导致图像尺寸的变小,使用上采样方式对图像尺寸进行恢复。
FCN主要用于语义分割,语音分割需要将各个物体的边界完全区分出来,在语义分割中我们需要将视觉输入分为不同的语义可解释类别。
从本质上来说:语音分割就是将图片上的所有像素点进行分类;从效果上来说,就类似于抠图,即对图像中不同的物体进行抠图。
核心思想:
- 不含全连接层的全卷积网络,可适应任意尺寸输入;
- 反卷积层增大图像尺寸,输出精细结果;
- 结合不同深度层结果的跳级结构,确保鲁棒性和精确性。
4.低级特征、中级特征、高级特征
低级特征: 低级特征指的是图像中的一些基本特征,如边缘、纹理、颜色等
。这些特征通常由输入图像的原始像素值通过滤波器提取得到。低级特征可以帮助我们捕捉图像中的局部细节和基本结构
,但对于整体图像的语义信息并不具备很强的表达能力。
中级特征: 中级特征是对低级特征的进一步抽象和组合,它们表示了图像中更大范围的结构和模式,比如纹理区域、简单的物体形状等
。中级特征通常由多个低级特征的组合而成,能够对局部结构进行一定程度的理解,并开始具备一定的语义信息。
高级特征: 高级特征是对图像整体语义信息的抽象和表达,它们代表了图像中更加抽象和复杂的概念,例如物体类别、场景类别等
。高级特征通过对中级特征的进一步组合和抽象而来,能够更好地描述图像的语义内容,对于图像分类、目标检测和语义分割等任务具有重要作用。
如图,先是低级特征,然后逐步提取出了高级特征。
5.多通道。N输入,M输出是如何实现的?
一个卷积每次只能提取一种类型的特征,即输出一张特征图,如果想要每一个卷积层能够提取多种不同类型的特征,会需要多个卷积核,经过卷积运算后会输出多张特征图,不同的特征图对应不同类型的特征。
注意对应关系:
每个卷积核的通道数要与这一层输入图像的通道数相同。
输出的通道数与这一层用的卷积核的个数一样。(一个卷积核(不管有几个通道)最终会形成一个单通道
)。
如下图,如果想要输入通道为n,输出通道为m,那么需要有m个卷积核,每个卷积核的通道数为n。
6.1×1的卷积核有什么作用
1)减少计算量与参数量
如下图,输入为
192
∗
28
∗
28
192*28*28
192∗28∗28,想得到
32
∗
28
∗
28
32*28*28
32∗28∗28的输出,先经过
1
∗
1
1*1
1∗1的卷积核再经过
5
∗
5
5*5
5∗5的卷积核,比直接经过
5
∗
5
5*5
5∗5的卷积核的计算量要少很多。
如下图,输入为
192
∗
16
∗
16
192*16*16
192∗16∗16,想得到
28
∗
16
∗
16
28*16*16
28∗16∗16的输出,先经过
1
∗
1
1*1
1∗1的卷积核再经过
3
∗
3
3*3
3∗3的卷积核,比直接经过
3
∗
3
3*3
3∗3的卷积核的参数量要少很多。
2)特征通道的升维和降维
因为输出的通道数与卷积核个数有关,通过设置合适的卷积核个数就能保证在图像尺寸不变的情况下,对图像进行升维和降维。
但是我感觉使用其他大小的卷积核也能实现从输入通道到输出通道的升维与降维,个人感觉这不能算是
1
∗
1
1*1
1∗1卷积核特有的作用,但由于我搜到的所有博客都写了这一点,我这里就先写上(也可能是自己理解得不充分)。
3)增加网络深度(增加非线性映射次数)
在网络中增加
1
∗
1
1*1
1∗1的卷积核相当于网络的层数也增加了,网络更深了。每个卷积之后要进行激活,相当于又增加了一个非线性层,有利于提高网络的能力。
4)跨通道的信息交互
1x1卷积核只有一个参数,当它作用在多通道的feature map上时,相当于不同通道对应位置的一个线性组合,但是这样输出的feature map就是多个通道的整合信息了,能够使网络提取的特征更加丰富。
二、使用CNN进行XO识别
1.复现参考资料中的代码
1)数据预处理
下载数据集,并将数据集划分为训练集和测试集
训练集(1700张图片):850个X,850个O。
测试集(300张图片):150个X,150个O。
代码:
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
import matplotlib.pyplot as plt
transforms = transforms.Compose([
transforms.ToTensor(), # 把数据转换成Tensor类型,并归一化到[0,1]之间,
# 并将图像的维度顺序从HWC(高度、宽度、通道)转换为CHW(通道、高度、宽度)。
transforms.Grayscale(1) # 将图像转换为灰度图像。1表示使用默认的加权平均法进行转换
])
#加载文件夹结构的图像数据集的类
data_train = datasets.ImageFolder('XO_train_data', transforms)
data_test = datasets.ImageFolder('XO_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 #images 的形状为 (batch_size, channels, height, width),labels 的形状为 (batch_size,)
print(images.shape)
print(labels.shape)
print(images)
print(labels)
break
for i, data in enumerate(test_loader):
images, labels = data
print(images.shape)
print(labels.shape)
break
运行结果:
label是如何确定的?
datasets.ImageFolder类会假设文件夹的组织结构符合以下规则:每个子文件夹代表一个类别
,文件夹的名字为类别的名称,而文件夹中的每个文件则被认为是属于该类别。这种结构常用于图像分类任务。
例如: ImageFolder会将 “circles_sm” 文件夹中的图像标签设为 0,将 “crosses_sm” 文件夹中的图像标签设为 1。这样,加载后的train_loader和test_loader中的每个数据样本就会包含图像数据及其对应的标签。
可视化数据集
import matplotlib.pyplot as plt
a=0
plt.figure()
index=0
for i in labels:
if i == 0 and a<5:
plt.subplot(1,5,1+a)
plt.imshow(images[index].data.squeeze(),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(1,5,1+a)
plt.imshow(images[index].data.squeeze(),cmap='gray')
plt.title('crosses '+str(a+1))
a+=1
if a==5:
break
index+=1
plt.show()
运行结果:
2)模型构建
代码:
import torch.nn as nn
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):
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
注意前一层与后一层图像通道数、神经元个数的对应关系.
卷积层:
池化层:
注意:计算的时候是
下取整
。
3)模型训练
model = CNN()
loss = nn.CrossEntropyLoss()
opti = torch.optim.SGD(model.parameters(), lr=0.1)
epochs = 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('[%d %5d] loss: %.3f' % (epoch + 1, i + 1, total_loss / 100))
total_loss = 0.0
print('finished train')
# 保存模型
torch.save(model, 'model.pth') # 保存的是模型, 不止是w和b权重值
运行结果:
采用小批量随机梯度下法对模型进行训练。总共训练10轮,每一轮中批大小为64。在每一轮训练中,每训练10个样本输出一次当前模型的损失。
最终损失收敛到0.001,训练效果不错。
4)模型测试
import matplotlib.pyplot as plt
# 读取模型
model_load = torch.load('model.pth')
# 读取一张图片 images[0],测试
print("labels[0] truth:\t", labels[0])
x = images[0].unsqueeze(0) #x形状由(1,116,116)变成(1,1,116,116),直接写x = images[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()
运行结果
5)计算模型的准确率
# 读取模型
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('Accuracy of the network on the test images: %f %%' % (100. * correct / total))
运行结果:
可以看出模型在测试集上的准确率达到了
99.6
99.6%
99.6以上,模型训练的效果还是比较好的。
_, predicted = torch.max(outputs.data, 1)中“_”是什么意思?
torch.max(outputs.data, 1)返回一个元组,包含两个张量:
每行最大值的值及每行最大值的索引
。通过将这个元组赋值给_, predicted,我们只保留了第二个张量,也就是每行最大值的索引,并将其存储在变量predicted中。
因为我们只关心预测的类别索引而不需要具体的最大值,所以使用下划线来表示我们不关心的部分。
2.重新设计网络结构
1)至少增加一个卷积层,卷积层达到三层以上
代码:
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=9, kernel_size=3)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(in_channels=9, out_channels=5, kernel_size=3)
self.relu = nn.ReLU()
self.conv3=nn.Conv2d(in_channels=5, out_channels=3, kernel_size=3)
self.fc1 = nn.Linear(in_features=12*12 * 3, out_features=1200)
self.fc2 = nn.Linear(in_features=1200, out_features=64)
self.fc3 = nn.Linear(in_features=64, out_features=2)
def forward(self, input):
output = self.pool(self.relu(self.conv1(input)))
output = self.pool(self.relu(self.conv2(output)))
output = self.pool(self.relu(self.conv3(output)))
output = output.view(-1, 12*12*3)
output = self.relu(self.fc1(output))
output = self.relu(self.fc2(output))
output = self.fc3(output)
return output
运行结果:
在最后加上了3个 3 ∗ 3 3*3 3∗3的5通道卷积核,再激活、池化,模型收敛得更快了,准确率达到了100%。
2)去掉池化层,对比“有无池化”的效果
代码:
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=9, kernel_size=3)
self.conv2 = nn.Conv2d(in_channels=9, out_channels=5, kernel_size=3)
self.relu = nn.ReLU()
self.fc1 = nn.Linear(in_features=112*112 *5, out_features=1200)
self.fc2 = nn.Linear(in_features=1200, out_features=64)
self.fc3 = nn.Linear(in_features=64, out_features=2)
def forward(self, input):
output = self.relu(self.conv1(input))
output = self.relu(self.conv2(output))
output = output.view(-1, 112*112*5)
output = self.relu(self.fc1(output))
output = self.relu(self.fc2(output))
output = self.fc3(output)
return output
运行结果:
在运行过程中,感觉去掉池化层之后,模型训练时间明显变长。真的是很慢很慢!而且去掉池化层之后,再训练相同轮次的情况下,模型的准确率也变低了。
3)修改“通道数”等超参数,观察变化
修改通道数
代码:
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=20, kernel_size=3)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(in_channels=20, out_channels=5, kernel_size=3)
self.relu = nn.ReLU()
self.fc1 = nn.Linear(in_features=27 * 27 * 5, out_features=1200)
self.fc2 = nn.Linear(in_features=1200, out_features=64)
self.fc3 = nn.Linear(in_features=64, out_features=2)
def forward(self, input):
output = self.pool(self.relu(self.conv1(input)))
output = self.pool(self.relu(self.conv2(output)))
output = output.view(-1, 27 * 27 * 5)
output = self.relu(self.fc1(output))
output = self.relu(self.fc2(output))
output = self.fc3(output)
return output
运行结果:
准确率方面没有太大改变,好像收敛更快了一点。
3.可视化
选择自己的最优模型
1)可视化部分卷积核和特征图
查看训练好的模型的卷积核
import matplotlib.pyplot as plt
# 读取模型
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(1,5, 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()
运行结果:
查看训练好的模型特征图
代码:
import torch.optim
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
transforms = transforms.Compose([
transforms.ToTensor(), # 把图片进行归一化,并把数据转换成Tensor类型
transforms.Grayscale(1) # 把图片 转为灰度图
])
data_train = datasets.ImageFolder('XO_train_data', transforms)
data_test = datasets.ImageFolder('XO_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 torch.nn as nn
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
import matplotlib.pyplot as plt
import numpy as np
# 读取模型
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] 维度变换
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)
# show 9 feature maps
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()
运行结果:
下图依次为卷积、激活、池化后的特征图
2)探索低级特征、中级特征、高级特征
代码:
from torch import nn
import matplotlib.pyplot as plt
from PIL import Image
import torchvision.transforms as transforms
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Conv2d(in_channels=1, out_channels=9, kernel_size=3)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(in_channels=9, out_channels=5, kernel_size=3)
self.conv3 = nn.Conv2d(in_channels=5, out_channels=3, kernel_size=3)
self.conv4 = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3)
self.relu = nn.ReLU()
# 为每一层注册钩子函数
self.feature_maps = {} # 存储特征图的字典
def hook_fn(layer_name):
def hook(module, input, output):
# 在钩子函数中保存特征图
self.feature_maps[layer_name] = output
return hook
self.conv1.register_forward_hook(hook_fn('conv1'))
self.conv2.register_forward_hook(hook_fn('conv2'))
self.conv3.register_forward_hook(hook_fn('conv3'))
self.conv4.register_forward_hook(hook_fn('conv4'))
def forward(self, input):
output = self.pool(self.relu(self.conv1(input)))
output = self.pool(self.relu(self.conv2(output)))
output = self.pool(self.relu(self.conv3(output)))
output = self.pool(self.relu(self.conv4(output)))
return output
# 加载图像并进行必要的预处理
image = Image.open('图4_灰度图.jpeg').convert('L') # 打开灰度图像
transform = transforms.Compose([
transforms.Resize((128,128)), # 调整图像大小为64*64
transforms.ToTensor() # 将图像转为张量
])
input_image = transform(image).unsqueeze(0) # 添加一个维度作为batch
# 创建模型实例
model = CNN()
# 前向传播
output = model(input_image)
# 可视化特征图
for layer_name, feature_map in model.feature_maps.items():
feature_map_np = feature_map.detach().numpy()
num_channels = feature_map_np.shape[1] # 获取通道数
num_plots = num_channels // 3 # 每个窗口显示3个通道的特征图
for i in range(num_plots):
plt.figure()
for j in range(3): # 每个窗口显示3个通道的特征图
channel_idx = i * 3 + j
plt.subplot(1, 3, j + 1)
plt.imshow(feature_map_np[0, channel_idx, :, :], cmap='viridis')
plt.title(f'{layer_name} - Channel {channel_idx}')
plt.show()
运行结果:
第一个卷积层:
第二个卷积层:
第三个卷积层:
第四个卷积层:
为啥感觉越往后的卷积层越分辨不出来是啥?(以下是从网上搜集到的原因)
在卷积神经网络中,随着网络层数的增加,特征图的尺寸会逐渐减小,这是由于卷积层中的池化操作(如代码中的MaxPool2d)导致的。池化操作的作用是通过取某个区域内的最大值或平均值来减小特征图的空间尺寸,从而减少参数数量和计算量。
当特征图尺寸减小时,每个特征图上的像素点表示的感受野也变得更大,因此每个像素点对应的感受野内的信息会更加丰富。这意味着越靠后的卷积层能够捕捉到更高级的特征。然而,由于特征图尺寸减小,每个像素点的空间位置信息也丢失了一部分,因此越靠后的卷积层的特征更加抽象,很难直观地还原出原始输入图像中的细节。
这种逐层抽象的过程是卷积神经网络的一个重要特性,它使得网络能够从低级的特征(如边缘、纹理)逐渐提取高级的语义特征(如物体形状、部件等),从而实现更精确的分类或识别任务。
因此,尽管越往后的卷积层的特征可能难以直观地还原出原始输入图像的细节,但它们提取的特征对于网络的整体性能和判别能力是至关重要的。如果需要更好的分辨率特征,可以减少池化操作的使用,或者使用一些特定的网络结构和技术来处理这个问题,例如跳跃连接、反卷积等。
收获:
通过这次试验了解到了局部感知、权值共享、池化的概念及其好处和坏处,知道了什么是全卷积,知道了1×1的卷积核的作用。通过复现代码使用CNN进行XO识别,以及自己动手画出网络结构图,亲自计算输入输出图像的尺寸,并可视化卷积核、特征图,对卷积层有了更深一层的理解。对Conv2d及Maxpool2d的使用也更加熟练了。