当前位置: 首页 > article >正文

【作业6】基于CNN的XO识别

目录

一、概念解释

二、使用CNN进行XO识别

1.代码复现

1.1数据集

1.2 模型构建

1.3 模型训练 

1.4 模型测试

完整代码:

1.5 查看训练好的模型的特征图

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

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

1. 低级特征(Low-Level Features)

2. 中级特征(Mid-Level Features)

3. 高级特征(High-Level Features)

2. 重新设计网络结构

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

 2.2 去掉池化层,对比有无池化效果

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

参考链接

总结与感悟


一、概念解释

二、使用CNN进行XO识别

1.代码复现

1.1数据集

数据集为XO数据集,共2000张XO图像,下载数据集,并将数据集划分为训练集和测试集:
训练集(1700张图片):850个X,850个O。
测试集(300张图片):150个X,150个O

        使用头文件 from torchvision import transforms, datasets  ,torchvision.transforms 模块提供了丰富的数据增强和预处理操作。PyTorch中torchvision库的详细介绍

搭建模型前对数据集进行预处理:

主要操作是

  • transforms.ToTensor():将图像转换为Tensor类型,并将图像的像素值从[0, 255]范围归一化到[0.0, 1.0]范围。
  • transforms.Grayscale(1):将图像转换为灰度图,参数1表示转换为单通道灰度图。

代码如下:

# 定义数据的预处理操作,将图片转换为灰度图并归一化为Tensor
transforms = transforms.Compose([
    transforms.ToTensor(),      # 将图片数据转换为Tensor类型,数值归一化到[0, 1]范围
    transforms.Grayscale(1)     # 将图片转换为单通道的灰度图
])

# 定义训练集和测试集路径
path_train = r'train_data'
path_test = r'test_data'

# 加载训练和测试数据集,应用数据预处理操作
data_train = datasets.ImageFolder(path_train, 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))

# 使用DataLoader将数据集分成小批量进行加载,batch_size=64表示每次加载64个样本
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

运行结果:

size of train_data: 1700
size of test_data: 300
torch.Size([64, 1, 116, 116])
torch.Size([64])
torch.Size([64, 1, 116, 116])
torch.Size([64])
  • 64batch_size,表示每个批次包含64个样本。
  • 1表示图像是单通道的,因为在预处理步骤中将图像转换为了灰度图。
  • 116, 116是图像的尺寸

ImageFolder假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类别名称。

像:

root_dir/
├── class1/
│   ├── img1.png
│   ├── img2.png
│   └── ...
├── class2/
│   ├── img1.png
│   ├── img2.png
│   └── ...
└── ...
  • ImageFolder 会根据文件夹名称自动生成类别标签,并为每个文件夹中的图片分配相应的标签。
  • 每个文件夹的索引作为该类别的标签。例如,上面的 class1 将被标记为 0class2 将被标记为 1
  • 返回的每一个样本是一个 (image, label) 元组,其中 image 是应用了 transform 操作后的图像数据,label 是对应的类别标签。

1.2 模型构建

 图片参考:NNDL 作业七 基于CNN的XO识别 

代码如下:(注释很清晰啦,这里面就不多解释拉)

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 第一层卷积,输入为灰度图像的单通道,输出为9个特征通道,卷积核大小为3x3,步长为1
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=9, kernel_size=3, stride=1)
        # 最大池化层,池化窗口为2x2,步长为2
        self.maxpool = nn.MaxPool2d(2, 2)
        # 第二层卷积,输入为9个通道,输出为5个特征通道,卷积核大小为3x3,步长为1
        self.conv2 = nn.Conv2d(in_channels=9, out_channels=5, kernel_size=3, stride=1)
        # 激活函数
        self.relu = nn.ReLU()

         '''
         # 第一层卷积输出特征图尺寸: (116 - 3) / 1 + 1 = 114
         池化:114/2=57
         # 第二层卷积输出特征图尺寸: (57 - 3) / 1 + 1 = 55
         池化:55/2=27.5上取整为27
         进入全连接输入大小为27 * 27 * 5
        '''
        
        # 全连接层1,输入大小为27*27*5(根据卷积和池化操作后的特征图尺寸),输出大小为1200
        self.fc1 = nn.Linear(27 * 27 * 5, 1200)
        # 全连接层2,输入大小为1200,输出大小为64
        self.fc2 = nn.Linear(1200, 64)
        # 输出层,输入大小为64,输出大小为2(用于二分类)x/o
        self.fc3 = nn.Linear(64, 2)

    # 前向传播过程
    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

 这里需要注意前一层与后一层图像通道数、神经元个数的对应关系,是如何计算得到的。

        简单总结了几点需要注意的,详细参考  卷积层输出大小尺寸计算

  • 卷积层会根据输入通道数和输出通道数调整特征图的深度,池化层不会改变通道数,只改变特征图的宽高。
  • 计算卷积输出,使用公式 (input size−kernel size+2×padding)/stride+1可以快速计算输出尺寸。
  • 展平操作的输入(进入全连接层的输入大小)为展平前得到的特征图的高、宽和通道数相乘。
  • 遇到奇数size时,应该向上取整

1.3 模型训练 

训练前需要实例化网络并选择好损失函数和优化器。

  • 对每个 epoch 进行循环,然后遍历数据集的每个小批量样本。
  • 前向传播:将图像数据 images 输入模型,计算输出 out
  • 计算损失:使用预测输出 out 和真实标签 label 计算损失 loss
  • 梯度清零:在反向传播前,清除上一轮的梯度累积。
  • 反向传播:调用 loss.backward() 计算梯度。
  • 参数更新:使用 optimizer.step() 更新模型参数。

训练完成后保存模型,torch.save(model.state_dict(), 'bestmodel'): 保存模型的参数到文件'bestmodel'中。这里注意保存的是模型的状态字典(state dict),而不是整个模型对象。

# 初始化网络、损失函数和优化器
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(train_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:
            # 每10个小批量数据打印一次损失
            print('Epoch[%d/%5d], loss: %.3f' % (epoch + 1, i + 1, running_loss / 100))
            running_loss = 0.0  # 重置损失值
print('训练结束')

# 保存模型的状态字典,包含模型的权重和偏置
torch.save(model.state_dict(), 'bestmodel')
size of train_data: 1700
size of test_data: 300
torch.Size([64, 1, 116, 116])
torch.Size([64])
torch.Size([64, 1, 116, 116])
torch.Size([64])
Epoch[1/   10], loss: 0.069
Epoch[1/   20], loss: 0.069
Epoch[2/   10], loss: 0.069
Epoch[2/   20], loss: 0.069
Epoch[3/   10], loss: 0.067
Epoch[3/   20], loss: 0.064
Epoch[4/   10], loss: 0.053
Epoch[4/   20], loss: 0.032
Epoch[5/   10], loss: 0.043
Epoch[5/   20], loss: 0.023
Epoch[6/   10], loss: 0.013
Epoch[6/   20], loss: 0.005
Epoch[7/   10], loss: 0.002
Epoch[7/   20], loss: 0.003
Epoch[8/   10], loss: 0.002
Epoch[8/   20], loss: 0.001
Epoch[9/   10], loss: 0.001
Epoch[9/   20], loss: 0.002
Epoch[10/   10], loss: 0.000
Epoch[10/   20], loss: 0.001
训练结束

 采用小批量随机梯度下法对模型进行训练。总共训练10轮,每一轮中批大小为64。在每一轮训练中,每训练10个样本输出一次当前模型的损失。最终损失收敛到0.003。

1.4 模型测试

# =======================加载并测试单张图片==========================
model_load = Net()  # 重新创建模型实例
model_load = torch.load('bestmodel') # 加载保存的模型状态
model_load.eval()  # 切换到评估模式

# 从测试集中取出一张图片
images, labels = next(iter(test_loader))
print("labels[0] truth:\t", labels[0].item())  # 打印真实标签
x = images[0].unsqueeze(0)  # 增加一个batch维度以匹配模型输入
with torch.no_grad():  # 禁用梯度计算,加快推理速度
    output = model_load(x)
    _, predicted = torch.max(output, 1)  # 获取预测类别
    print("labels[0] predict:\t", predicted.item())  # 打印预测标签

# 显示测试图像
img = images[0].data.squeeze().numpy()  # 将图像转换为numpy数组并去除batch维度
plt.imshow(img, cmap='gray')  # 显示为灰度图
plt.show()

# ============================测试模型在整个测试集上的准确率==========
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)
        correct += (predicted == labels).sum().item()

print('网络在测试图像上的准确率: %.2f%%' % (100. * correct / total))

 网络在整个测试集上的准确率:99.333333 %
_, predicted = torch.max(outputs.data, 1)中需要注意:
1、max括号内的第二个参数1是指定了要沿着哪个维度寻找最大值。在这里,表示沿着每个样本的类别输出维度,最后函数返回两个值:预测类别输出的概率最大值和对应的索引。
2、_是一个惯用的占位符,用于忽略函数返回的第一个值(即最大值本身),只保留了预测的类别索引。
注意读取模型使用命令model_load.load_state_dict(torch.load('bestmodel')) 并切换到评估,模式,model.eval()否则报错或警告

完整代码:
'''
@Author :lxy
@function : XOrecognition based in CNN
@date :2024/10/26
'''
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
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt

# 定义数据的预处理操作,将图片转换为灰度图并归一化为Tensor
transforms = transforms.Compose([
    transforms.ToTensor(),      # 将图片数据转换为Tensor类型,数值归一化到[0, 1]范围
    transforms.Grayscale(1)     # 将图片转换为单通道的灰度图
])

# 定义训练集和测试集路径
path_train = r'train_data'
path_test = r'test_data'

# 加载训练和测试数据集,应用数据预处理操作
data_train = datasets.ImageFolder(path_train, 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))

# 使用DataLoader将数据集分成小批量进行加载,batch_size=64表示每次加载64个样本
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 Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 第一层卷积,输入为灰度图像的单通道,输出为9个特征通道,卷积核大小为3x3,步长为1
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=9, kernel_size=3)
        # 最大池化层,池化窗口为2x2,步长为2
        self.maxpool = nn.MaxPool2d(2, 2)
        # 第二层卷积,输入为9个通道,输出为5个特征通道,卷积核大小为3x3,步长为1
        self.conv2 = nn.Conv2d(in_channels=9, out_channels=5, kernel_size=3 )
        # 激活函数
        self.relu = nn.ReLU()

        '''
                 # 第一层卷积输出特征图尺寸: (116 - 3) / 1 + 1 = 114
                 池化:114/2=57
                 # 第二层卷积输出特征图尺寸: (57 - 3) / 1 + 1 = 55
                 池化:55/2=27.5上取整为27
                 进入全连接输入大小为27 * 27 * 5
                '''
        # 全连接层1,输入大小为27*27*5(根据卷积和池化操作后的特征图尺寸),输出大小为1200
        self.fc1 = nn.Linear(27 * 27 * 5, 1200)
        # 全连接层2,输入大小为1200,输出大小为64
        self.fc2 = nn.Linear(1200, 64)
        # 输出层,输入大小为64,输出大小为2(用于二分类)x/o
        self.fc3 = nn.Linear(64, 2)

    # 前向传播过程
    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()  # 实例化网络模型
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(train_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:
            # 每10个小批量数据打印一次损失
            print('Epoch[%d/%5d], loss: %.3f' % (epoch + 1, i + 1, running_loss / 100))
            running_loss = 0.0  # 重置损失值
print('训练结束')

# 保存模型的状态字典,包含模型的权重和偏置
torch.save(model.state_dict(), 'bestmodel')

# =======================加载并测试单张图片==========================
model_load = Net()  # 重新创建模型实例
model.load_state_dict(torch.load('bestmodel'))
model_load.eval()  # 切换到评估模式

# 从测试集中取出一张图片
images, labels = next(iter(test_loader))
print("labels[0] truth:\t", labels[0].item())  # 打印真实标签
x = images[0].unsqueeze(0)  # 增加一个batch维度以匹配模型输入
with torch.no_grad():  # 禁用梯度计算,加快推理速度
    output = model_load(x)
    _, predicted = torch.max(output, 1)  # 获取预测类别
    print("labels[0] predict:\t", predicted.item())  # 打印预测标签

# 显示测试图像
img = images[0].data.squeeze().numpy()  # 将图像转换为numpy数组并去除batch维度
plt.imshow(img, cmap='gray')  # 显示为灰度图
plt.show()

# ============================测试模型在整个测试集上的准确率==========
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)  # 记录总样本数
        correct += (predicted == labels).sum().item()  # 统计预测正确的样本数

accuracy = 100. * correct / total
print(f'网络在整个测试集上的准确率: {accuracy:f}%')

1.5 查看训练好的模型的特征图

import torch.optim
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
import torch.nn as nn
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import numpy as np

# 数据预处理
transforms = transforms.Compose([
    transforms.ToTensor(),
    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
# 定义 CNN 模型
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)  # 保存经过 ReLU 激活后的特征图
        x = self.pool(x)
        outputs.append(x)  # 保存经过池化后的特征图
        x = self.conv2(x)
        outputs.append(x)
        x = self.relu(x)
        outputs.append(x)
        x = self.pool(x)
        outputs.append(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 = CNN()
model.load_state_dict(torch.load('bestmodel'))
print(model)

# 测试输入数据
x = images[0].unsqueeze(0)
out_put = model(x)

'''
查看训练好的模型特征图
'''
titles = ["Conv1 Output", "ReLU after Conv1", "MaxPool after Conv1",
          "Conv2 Output", "ReLU after Conv2", "MaxPool after Conv2"]  # 每层特征图的标题
for i, feature_map in enumerate(out_put):
    im = np.squeeze(feature_map.detach().numpy())
    im = np.transpose(im, [1, 2, 0])  # 调整通道维度

    plt.figure()
    plt.suptitle(titles[i])  # 设置每个特征图的标题
    num_filters = im.shape[2]  # 特征图的通道数(即滤波器个数)
    for j in range(num_filters):  # 显示每一层的特征图
        ax = plt.subplot(3, 3, j + 1)
        plt.imshow(im[:, :, j], cmap='gray')
        plt.axis(

 

     ​​​​​​ 

  

   

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

'''
查看训练好的模型的卷积核 
'''
# 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()

 

 

 

网络结构:

CNN(
  (conv1): Conv2d(1, 9, kernel_size=(3, 3), stride=(1, 1))
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (conv2): Conv2d(9, 5, kernel_size=(3, 3), stride=(1, 1))
  (relu): ReLU()
  (fc1): Linear(in_features=3645, out_features=1200, bias=True)
  (fc2): Linear(in_features=1200, out_features=64, bias=True)
  (fc3): Linear(in_features=64, out_features=2, bias=True)
)

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

由1.5展现出的训练好的特征图,可以看出

1. 低级特征(Low-Level Features)

低级特征通常是图像的基本组成部分,比如边缘、角点和纹理等。这些特征主要通过网络的第一层卷积层提取:

  • 卷积层1 (conv1):它的输入是灰度图像(单通道),卷积核大小为 3x3,输出为 9 个特征通道。对于识别X和O形状的轮廓非常重要。

2. 中级特征(Mid-Level Features)

中级特征是由多个低级特征组合而成的更复杂的特征。这些特征在第二层卷积层中提取:

  • 卷积层2 (conv2):该层输入的是从第一层卷积提取的 9 个特征通道,输出为 5 个特征通道。此层卷积能提取更复杂的形状和局部模式,如“X”和“O”的部分曲线轮廓等等。

3. 高级特征(High-Level Features)

高级特征是指模型在更高层次上提取的抽象特征,通常与具体的分类任务直接相关。这些特征是在全连接层中提取的:

  • 全连接层1 (fc1)全连接层2 (fc2):全连接层能够将提取的特征整合起来,形成对整体图像的理解,最终用于判断当前状态是X还是O。

  • 输出层 (fc3):最终输出层将这些高级特征映射到具体的类别(例如“X”和“O”),进行分类。

2. 重新设计网络结构

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

# 定义卷积神经网络结构
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 第一层卷积,输入为灰度图像的单通道,输出为9个特征通道,卷积核大小为3x3,步长为1
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=9, kernel_size=3)
        # 最大池化层,池化窗口为2x2,步长为2
        self.maxpool = nn.MaxPool2d(2, 2)
        # 第二层卷积,输入为9个通道,输出为5个特征通道,卷积核大小为3x3,步长为1
        self.conv2 = nn.Conv2d(in_channels=9, out_channels=5, kernel_size=3 )
        # 第三层卷积,输入为5个通道,输出为5个特征通道,卷积核大小为3x3,步长为1
        self.conv3 = nn.Conv2d(in_channels=5, out_channels=5, kernel_size=3)
        # 激活函数
        self.relu = nn.ReLU()
        '''
         # 第一层卷积输出特征图尺寸: (116 - 3) / 1 + 1 = 114
         池化:114/2=57
         # 第二层卷积输出特征图尺寸: (57 - 3) / 1 + 1 = 55
         池化:55/2=27.5上取整为27
         # 第二层卷积输出特征图尺寸: (27 - 3) / 1 + 1 = 25
         池化:25/2=12.5 上取整 12
         进入全连接输入大小为12 * 12 * 5
        '''

        # 全连接层1,输入大小为12*12*5(根据卷积和池化操作后的特征图尺寸),设置输出大小为1200
        self.fc1 = nn.Linear(12 * 12 * 5, 1200)
        # 全连接层2,输入大小为1200,输出大小为64
        self.fc2 = nn.Linear(1200, 64)
        # 输出层,输入大小为64,输出大小为2(用于二分类)x/o
        self.fc3 = nn.Linear(64, 2)

    # 前向传播过程
    def forward(self, x):
        # 第一层卷积+激活+池化
        x = self.maxpool(self.relu(self.conv1(x)))
        # 第二层卷积+激活+池化
        x = self.maxpool(self.relu(self.conv2(x)))
        # 第三层卷积+激活+池化
        x = self.maxpool(self.relu(self.conv3(x)))
        # 展平操作,将多维特征图展平为一维
        x = x.view(-1, 12 * 12 * 5)
        # 第一个全连接层+激活
        x = self.relu(self.fc1(x))
        # 第二个全连接层+激活
        x = self.relu(self.fc2(x))
        # 输出层,不加激活函数,用于分类任务
        x = self.fc3(x)
        return x
size of train_data: 1700
size of test_data: 300
torch.Size([64, 1, 116, 116])
torch.Size([64])
torch.Size([64, 1, 116, 116])
torch.Size([64])
Epoch[1/   10], loss: 0.069
Epoch[1/   20], loss: 0.069
Epoch[2/   10], loss: 0.069
Epoch[2/   20], loss: 0.068
Epoch[3/   10], loss: 0.061
Epoch[3/   20], loss: 0.049
Epoch[4/   10], loss: 0.018
Epoch[4/   20], loss: 0.013
Epoch[5/   10], loss: 0.004
Epoch[5/   20], loss: 0.004
Epoch[6/   10], loss: 0.003
Epoch[6/   20], loss: 0.001
Epoch[7/   10], loss: 0.001
Epoch[7/   20], loss: 0.001
Epoch[8/   10], loss: 0.001
Epoch[8/   20], loss: 0.000
Epoch[9/   10], loss: 0.000
Epoch[9/   20], loss: 0.000
Epoch[10/   10], loss: 0.000
Epoch[10/   20], loss: 0.000

训练结束
labels[0] truth:	 0
labels[0] predict:	 0
网络在整个测试集上的准确率: 100.000000%

添加5个3*3的卷积核,不改变通道数 ,网络的准确率达到了100%

 2.2 去掉池化层,对比有无池化效果

class Net(nn.Module):
    def __init__(self):
        super(Net, 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

 输出;

size of train_data: 1700
size of test_data: 300
torch.Size([64, 1, 116, 116])
torch.Size([64])
torch.Size([64, 1, 116, 116])
torch.Size([64])
Epoch[1/   10], loss: 0.070
Epoch[1/   20], loss: 0.069
Epoch[2/   10], loss: 0.069
Epoch[2/   20], loss: 0.069
Epoch[3/   10], loss: 0.067
Epoch[3/   20], loss: 0.058
Epoch[4/   10], loss: 0.069
Epoch[4/   20], loss: 0.069
Epoch[5/   10], loss: 0.069
Epoch[5/   20], loss: 0.069
Epoch[6/   10], loss: 0.069
Epoch[6/   20], loss: 0.069
Epoch[7/   10], loss: 0.068
Epoch[7/   20], loss: 0.070
Epoch[8/   10], loss: 0.069
Epoch[8/   20], loss: 0.069
Epoch[9/   10], loss: 0.069
Epoch[9/   20], loss: 0.070
Epoch[10/   10], loss: 0.068
Epoch[10/   20], loss: 0.068
训练结束
labels[0] truth:	 1
labels[0] predict:	 1
网络在整个测试集上的准确率: 50.000000%

Process finished with exit code 0

没有池化层,明显能感到训练过程变得更慢,训练相同轮次的情况下,模型的准确率也变低了。

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

增加通道数至40

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=20, kernel_size=3)
        self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
        self.conv2 = nn.Conv2d(in_channels=20, out_channels=40, kernel_size=3)
        self.relu = nn.ReLU()

        self.fc1 = nn.Linear(in_features=27 * 27 * 40, 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.maxpool(self.relu(self.conv1(input)))
        output = self.maxpool(self.relu(self.conv2(output)))
        output = output.view(-1, 27 * 27 * 40)
        output = self.relu(self.fc1(output))
        output = self.relu(self.fc2(output))
        output = self.fc3(output)
        return output

out_channels (通道数)表示卷积层中卷积核的数量,也就是提取特征的数量。

一个卷积核可以提取一种特定的特征,比如边缘、角落等。

当我们需要从输入数据中提取多种不同的特征时,就需要使用多个卷积核,每个卷积核都可以提取一种不同的特征。而 out_channels 的数量就是卷积核的数量。

  • 少的通道数可能导致模型在捕捉特征时的表现较差,因为信息传递的通道较少。
  • 多的通道数通常可以捕捉更多的特征,但也会增加模型的计算复杂性,可能导致过拟合。
size of train_data: 1700
size of test_data: 300
torch.Size([64, 1, 116, 116])
torch.Size([64])
torch.Size([64, 1, 116, 116])
torch.Size([64])
Epoch[1/   10], loss: 0.069
Epoch[1/   20], loss: 0.068
Epoch[2/   10], loss: 0.058
Epoch[2/   20], loss: 0.059
Epoch[3/   10], loss: 0.013
Epoch[3/   20], loss: 0.007
Epoch[4/   10], loss: 0.005
Epoch[4/   20], loss: 0.004
Epoch[5/   10], loss: 0.003
Epoch[5/   20], loss: 0.002
Epoch[6/   10], loss: 0.002
Epoch[6/   20], loss: 0.002
Epoch[7/   10], loss: 0.001
Epoch[7/   20], loss: 0.001
Epoch[8/   10], loss: 0.000
Epoch[8/   20], loss: 0.001
Epoch[9/   10], loss: 0.001
Epoch[9/   20], loss: 0.000
Epoch[10/   10], loss: 0.000
Epoch[10/   20], loss: 0.000
训练结束
labels[0] truth:	 1
labels[0] predict:	 1
网络在整个测试集上的准确率: 100.000000%

 增加通道数可以使模型更好,损失更小,收敛的更快,但也会使训练时间增长毕竟参数增多了,让我感受到了以后实验用到复杂网络时的运行个几天的可怕了。。。。

参考链接

卷积神经网络的运作原理

pytorch 状态字典:state_dict

输入通道数 和 输出通道数 的理解

老师参考代码【2021-2022 春学期】人工智能-作业6:CNN实现XO识别

NNDL 作业6:基于CNN的XO识别----一个很认真的学长

PyTorch中torchvision库的详细介绍

卷积层尺寸计算

总结与感悟

        1.这次作业首先让我感受到了ImageFolder 的便捷之处,它简化了数据集加载的过程,自动将文件夹结构转化为标签,大大减少了手动标记的复杂性。

        2.在书写卷积网络时要会计算conv层、激活层、flatten层的输入输出参数,卷积传入的参数按照in_channels, out_channels, kernel_size stride的顺序写,激活层不改变通道数。

        3. 当使用torch.save(model.state dict(), PATH)保存模型时,仅保存学习到的参数,所以加载model.state dict,用以下命令

model= TheModelClass("args,"*kwargs)
modelload state dict(torch.load(PATH))
model.eval()

       备注:model.load state dict的操作对象是 一个具体的对象,而不能是文件名。

        4、了解到了Torch.max中的参数并不是以往学过的max函数单纯求最值,还可以指定某个维度方向来求最值。

        5、掌握了卷积神经网络书写的架构(感觉还是比较简单的),认识到池化层的重要性,能保留主要的特征同时减少参数(降维,效果类似PCA)和计算量,防止过拟合,提高模型泛化能力,需要会计算每次操作后特征图的大小尺寸(真的很重要)。并了解到几个新的超参数:in_channels、out_channels、kernel_size。


http://www.kler.cn/news/367497.html

相关文章:

  • ThinkPad T480拆机屏幕改装:便携式显示器DIY指南
  • Redis 哨兵 总结
  • 传输层UDP
  • 量子变分算法 (python qiskit)
  • 10.22.2024刷华为OD C题型(三)--for循环例子
  • 研发运营一体化(DevOps)能力成熟度模型
  • 为什么会有树这样的数据结构,使用树有什么好处 和其他数据结构对比
  • Qt:QtCreator使用
  • 可以拖动屏幕的简单页面播放示例
  • 深入探讨TCP/IP协议基础
  • 【C++】—— 模板进阶
  • 数字加% 循环后两个都变了只能深拷贝
  • 《计算机原理与系统结构》学习系列——处理器(中)
  • Linux:socket实现两个进程之间的通信
  • #单体到微服务架构服务演化过程
  • Mermaid流程图完全指南
  • 2024年10月25日练习(双指针算法)
  • Redis 主从同步 问题
  • python一键运行所有bat脚本
  • 机器学习(10.14-10.20)(Pytorch GRU的原理及其手写复现)
  • P1588 [USACO07OPEN] Catch That Cow S
  • Unity C#脚本的热更新
  • 单细胞 | 转录因子足迹分析
  • Docker容器间通信
  • 深入了解 MySQL 中的 INSERT ... SELECT 语句
  • iOS弹出系统相册选择弹窗