目录
一、概念解释
二、使用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])
64
是batch_size
,表示每个批次包含64个样本。1
表示图像是单通道的,因为在预处理步骤中将图像转换为了灰度图。116, 116
是图像的尺寸
ImageFolder假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类别名称。
像:
root_dir/ ├── class1/ │ ├── img1.png │ ├── img2.png │ └── ... ├── class2/ │ ├── img1.png │ ├── img2.png │ └── ... └── ...
ImageFolder
会根据文件夹名称自动生成类别标签,并为每个文件夹中的图片分配相应的标签。- 每个文件夹的索引作为该类别的标签。例如,上面的
class1
将被标记为0
,class2
将被标记为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.001。
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. 低级特征
低级特征通常是图像的基本组成部分,比如边缘、角点和纹理等。这些特征主要通过网络的第一层卷积层提取:
-
卷积层1 (
conv1
):它的输入是灰度图像(单通道),卷积核大小为 3x3,输出为 9 个特征通道。对于识别X和O形状的轮廓非常重要。
2. 中级特征
中级特征是由多个低级特征组合而成的更复杂的特征。这些特征在第二层卷积层中提取:
-
卷积层2 (
conv2
):该层输入的是从第一层卷积提取的 9 个特征通道,输出为 5 个特征通道。此层卷积能提取更复杂的形状和局部模式,如“X”和“O”的部分曲线轮廓等等。
3. 高级特征
高级特征是指模型在更高层次上提取的抽象特征,通常与具体的分类任务直接相关。这些特征是在全连接层中提取的:
-
全连接层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%
增加通道数可以使模型更好,损失更小,收敛的更快,但也会使训练时间增长毕竟参数增多了,让我感受到了以后实验用到复杂网络时的运行个几天的可怕了。。。。
参考链接
老师参考代码【2021-2022 春学期】人工智能-作业6:CNN实现XO识别
总结与感悟
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。