环境
系统环境:
环境 | 配置 |
---|---|
cuda | 11.8 |
python | 3.8 |
torch | 2.0.1+cu118 |
主要步骤
主要实现包括:
- 导入所需的PyTorch库。
- 定义数据预处理转换,将图像转换为张量并进行标准化。
- 加载MNIST训练集和测试集,并应用预处理转换。
- 创建训练集和测试集的数据加载器,用于批量加载数据。
- 定义CNN模型的类。在这个示例中,模型包含两个卷积层和两个全连接层。
- 实例化CNN模型。
- 定义损失函数和优化器。这里使用交叉熵损失和Adam优化器。
- 设置训练的总轮数和设备类型(GPU或CPU)。
- 将模型移动到指定的设备上。 在每个epoch中进行训练。
- 首先,将模型设置为训练模式,然后遍历训练数据加载器。
- 将输入数据和标签移动到指定的设备上。
- 清零优化器的梯度。
- 前向传播:通过模型获取输出。
- 计算损失。
- 反向传播:计算梯度并更新模型参数。
- 在每个epoch结束后,在测试集上评估模型的准确率。首先,将模型设置为评估模式,然后遍历测试数据加载器。
- 将输入数据和标签移动到指定的设备上。
- 前向传播:通过模型获取输出。
- 使用预测结果和真实标签计算准确率。
- 打印当前epoch的测试准确率。
CNN模型
import torch.nn as nn
# 定义用于进行训练的CNN模型,模型包含两个卷积层和两个全连接层。
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
# 因为输入图片为1*28*28,说明为单通道图片,大小为28*28。
# 1: 输入通道数(input channels)表示输入数据的通道数,这里的值为1,说明输入是单通道的图像。
# 32: 输出通道数(output channels)表示卷积层的输出通道数,这里的值为32,说明该卷积层会产生32个不同的特征图作为输出。
# kernel_size = 3: 卷积核大小 表示卷积核的尺寸大小。在这种情况下,卷积核的大小为3x3,即3行3列。
# stride = 1: 步幅(stride)表示卷积操作时滑动卷积核的步幅大小。在这里,步幅为1,意味着卷积核每次在水平和垂直方向上都以1个像素的距离滑动。
# padding = 1: 填充(padding)表示在输入图像周围添加额外的零值像素来控制输出图像的尺寸。在这里,填充为1,意味着在输入图像的周围添加1个像素宽度的零值填充。
# 所以输出为 28+2(padding1+1=2)-(3-1)(kernek_size-1)=28,即64,32,28,28
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
#表示在CNN模型中定义了一个ReLU激活函数层。
# ReLU(Rectified Linear Unit)是一种常用的非线性激活函数,它的定义是 f(x) = max(0, x),即将小于零的输入值变为零,而大于等于零的输入值保持不变。
# 在深度学习中,ReLU激活函数被广泛应用于神经网络的隐藏层,作为引入非线性性质的关键组件。ReLU的主要作用是引入非线性映射,使得神经网络能够学习复杂的非线性关系。
# 输出大小与之前一样
self.relu = nn.ReLU()
# 最大池化是一种用于降低特征图维度的操作,常用于卷积神经网络中。它将输入的特征图划分为不重叠的矩形区域(通常是2x2的窗口),然后在每个区域中选择最大的值作为输出。这样可以减少特征图的空间维度,并保留最显著的特征。
# kernel_size = 2:池化窗口大小:表示池化操作使用的窗口大小。在这里,池化窗口的大小为2x2,即2行2列的窗口。
# stride = 2:步幅:表示在池化操作时滑动池化窗口的步幅大小。在这里,步幅为2,意味着池化窗口每次在水平和垂直方向上都以2个像素的距离滑动。
# 最大池化层通常紧跟在卷积层之后,用于减小特征图的尺寸,同时保留主要的特征。它有助于减少模型的参数数量,提高计算效率,并具有一定的平移不变性。
# 输出大小为原图片大小长宽除以2
self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
# 32: 输入通道数(input channels)表示输入数据的通道数,为之前产生的32个不同的特征图
# 64: 输出通道数(output channels)表示卷积层的输出通道数,这里的值为64,说明该卷积层会产生64个不同的特征图作为输出。
# kernel_size = 3: 卷积核大小 表示卷积核的尺寸大小。在这种情况下,卷积核的大小为3x3,即3行3列。
# stride = 1: 步幅(stride)表示卷积操作时滑动卷积核的步幅大小。在这里,步幅为1,意味着卷积核每次在水平和垂直方向上都以1个像素的距离滑动。
# padding = 1: 填充(padding)表示在输入图像周围添加额外的零值像素来控制输出图像的尺寸。在这里,填充为1,意味着在输入图像的周围添加1个像素宽度的零值填充。
# 所以输出为 14+2(padding1+1=2)-(3-1)(kernek_size-1)=28,即64,64,14,14
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
# 全连接层是深度学习模型中常用的一种层类型,也称为线性层或密集连接层。它的作用是将前一层的所有输入与当前层的每个神经元进行连接,通过权重和偏置进行线性变换,然后将结果传递给激活函数进行非线性映射。
# 参数解释如下:
# 7 * 7 * 64:输入特征的维度 表示前一层的输出特征的维度。在这里,这个维度的值是7 * 7 * 64,说明输入特征是一个7x7的图像,具有64个通道。
# 10:输出特征的维度 表示当前层输出特征的维度。在这里,这个维度的值是10,说明全连接层将产生一个包含10个元素的输出。
self.fc = nn.Linear(7 * 7 * 64, 10)
def forward(self, x):
# print(x.shape) torch.Size([64, 1, 28, 28])
out = self.conv1(x)
# print(out.shape) torch.Size([64, 32, 28, 28])
out = self.relu(out)
# print(out.shape) torch.Size([64, 32, 28, 28])
out = self.maxpool(out)
# print(out.shape) torch.Size([64, 32, 14, 14])
out = self.conv2(out)
# print(out.shape) torch.Size([64, 64, 14, 14])
out = self.relu(out)
# print(out.shape) torch.Size([64, 64, 14, 14])
out = self.maxpool(out)
# print(out.shape) torch.Size([64, 64, 7, 7])
# out.view(out.size(0), -1)的含义是将张量 out 进行形状变换。其中,第一个维度的大小保持不变(即 out.size(0)),而剩余维度的大小会根据张量的元素数量自动计算得出。即变成64*7*7=3136
out = out.view(out.size(0), -1)
# print(out.shape) torch.Size([64, 3136])
out = self.fc(out)
# print(out.shape) torch.Size([64, 10])
return out
数据预处理
if __name__ == '__main__':
print_hi('PyTorch')
train_dataset = datasets.MNIST(root='./data', train=True, download=True)
mean, std = cal_mean_and_std(train_dataset)
# 定义数据预处理转换,定义数据预处理转换,将图像转换为张量并进行标准化
transform = transforms.Compose([
transforms.ToTensor(), # 将图像转换为张量
transforms.Normalize(mean, std) # 标准化图像数据 output = (input - mean) / std
])
# 加载MNIST训练集和测试集,应用预处理转换
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
# 创建数据加载器,用于批量加载数据
batch_size = 64
print("batch_size: ", batch_size)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
print("训练集数据大小", len(train_loader))
print("验证集数据大小", len(test_loader))
# 获取一个小批量数据并打印其形状
dataiter = iter(train_loader)
images, labels = next(dataiter)
print("图片大小", images.shape) # 打印图像数据的形状,如 (64, 1, 28, 28)
print("标签数据大小", labels.shape) # 打印标签数据的形状,如 (64)
其中计算数据集的平均数与标准差的函数为:
def cal_mean_and_std(dataset):
data = [np.array(image) / 255 for image, _ in dataset]
data = np.stack(data, axis=0)
# 计算三维数组 data 沿着前三个维度的平均值,即计算所有元素的平均值
mean = np.mean(data, axis=(0, 1, 2))
# 计算三维数组 data 沿着前三个维度的平均值,即计算所有元素的平均值
std = np.std(data, axis=(0, 1, 2))
print("mean:", mean, "std:", std)
return mean, std
模型训练与保存
每轮训练后在训练集和验证集上分别验证分类准确率,同时保存每轮的模型文件:
# 创建CNN模型实例
model = CNN()
# 定义交叉熵损失函数和Adam优化器
criterion = nn.CrossEntropyLoss()
print("损失函数", criterion)
optimizer = optim.Adam(model.parameters(), lr=0.001)
print("优化器", optimizer)
# 训练模型
num_epochs = 100
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print("使用设备", device)
model.to(device)
best_accuracy = 0
best_epoch = 0
# epoch大循环
for epoch in range(num_epochs):
model.train()
# batch小循环: images[64, 1, 28, 28], labels[64]
start_time = time.time()
for images, labels in tqdm(train_loader, desc='训练ing...'):
images = images.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
train_time = time.time() - start_time
# 在每个epoch结束后计算模型在训练集上的准确率
model.eval()
total0 = 0
correct0 = 0
start_time = time.time()
with torch.no_grad():
for images, labels in tqdm(train_loader, desc='训练集准确性测试ing...'):
images = images.to(device)
labels = labels.to(device)
# outputs_size: 64,10
outputs = model(images)
# predicted_size: 64, 每个image输出的10个元素的tensor中最大的元素的下标,只关注下标!!
# outputs.data 是模型的输出张量,它的形状为 (batch_size, num_classes),其中 batch_size=64 是每个小批量数据的大小,num_classes 是分类问题的类别数。因此,torch.max(outputs.data, 1) 将返回 (max_values, max_indices) 二元组,其中 max_values 是每一行中的最大值,max_indices 是每一行中最大值所在的列的下标
confidence_values, predicted = torch.max(outputs.data, 1) # confidence_values是最大值的大小,即置信度值
# 计算总数目
total0 += labels.size(0)
# 计算正确数目
correct0 += (predicted == labels).sum().item()
train_accuracy0 = 100 * correct0 / total0
test_time0 = time.time() - start_time
# 在每个epoch结束后计算模型在测试集上的准确率
model.eval()
total = 0
correct = 0
start_time = time.time()
with torch.no_grad():
for images, labels in tqdm(test_loader, desc='验证集准确性测试ing...'):
images = images.to(device)
labels = labels.to(device)
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
test_accuracy = 100 * correct / total
test_time1 = time.time() - start_time
tqdm.write(f"Time [{time.time()}], Epoch [{epoch + 1}/{num_epochs}], Train Accuracy: {train_accuracy0:.2f}%, "
f"Test Accuracy: {test_accuracy:.2f}%, Train Time: {train_time:.2f}s, "
f"Test_train_dateset Time: {test_time0:.2f}s, "
f"Test_test_dateset Time: {test_time1:.2f}s")
torch.save(model.state_dict(), f'res/process/model_epoch_{epoch+1}.pkl')
保存验证集上效果最好的模型为pkl:
# 在每个epoch结束时,检查模型在验证集上的准确性是否最好
if test_accuracy > best_accuracy:
best_accuracy = test_accuracy
if os.path.exists(f'res/best_model_epoch_{best_epoch}.pkl'):
# 如果存在,则删除该文件
os.remove(f'res/best_model_epoch_{best_epoch}.pkl')
best_epoch = epoch + 1
print(f"save new best_model_epoch: {best_epoch}")
# 保存当前最好的模型
torch.save(model.state_dict(), f'res/best_model_epoch_{best_epoch}.pkl')
训练过程保存
保存训练过程中的准确度变化为csv文件
训练前:
file_name = 'res/acc.csv' # csv 文件名
file_path = os.path.abspath(os.path.join(os.getcwd(), file_name)) # csv 文件路径
if os.path.exists(file_path): # 判断文件是否存在
os.remove(file_path) # 如果文件存在则删除
with open(file_path, 'a+', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['epoch','train_acc','test_acc'])
每个Epoch结束:
# 保存loss
with open(file_path, 'a+', newline='') as csvfile:
writer = csv.writer(csvfile)
writer.writerow([epoch + 1, train_accuracy0, test_accuracy])
训练结果
训练过程:
预处理阶段:
训练过程:
这里GPU负载还挺高的哈哈
保存的最好的模型文件是在第55轮
此时准确度测试集99.29%,训练集99.99%:
保存的精确度文件acc.csv:
绘制训练集与验证集精确度随Epoch变化如下:
发现20轮左右就收敛了