根据手写数据集(如下图所示)训练模型
关于算法(神经网络)
1.明确神经网络的输入输出,不强调内部结构
神经网络:看成能强大地拟合复杂的函数映射关系,拥有“无穷多”参数(权重和偏置)的一个网络(实际上参数的数量取决于网络的架构,包括层数和每层的神经元数量。参数过多可能会导致过拟合,而参数过少可能会导致欠拟合)
欠拟合是什么呢?模型因为太简单或缺乏足够的参数,而无法充分学习和模拟数据中的复杂性和多样性。
像用一个非常简单的尺子来测量不同形状的物体。这个尺子只有一条直线,没有任何弯曲或可调节的部分。当你用它来测量直线或简单的曲线时,它可能还能应付,但当你试图用它来测量更复杂的形状,比如螺旋形或波浪形时,尺子就显得无能为力了。
过拟合是什么呢?模型太复杂,学习了数据中的每一个细节,包括噪声,而没有学习到泛化的模式
- 模型在训练数据上表现很好,但在新的、未见过的数据上表现差。
- 模型学习了训练数据中的噪声,而不仅仅是潜在的数据分布。
再用一个比喻来说明过拟合:
如果一个小孩子花费大量时间来建造一个非常详细和复杂的乐高模型,他可能会记住每一个小块的位置和颜色。但如果他尝试用同样的小块去建造另一个不同的模型,他可能会遇到困难,因为他只记住了第一个模型的细节。这就像是一个神经网络在训练数据上训练得过于完美,以至于它学习到了数据中的每一个小细节和随机波动,而没有学习到更普遍的规则。
训练:网络模型搭好后,通过调整网络的权重和偏置参数,优化算法(如梯度下降)来最小化损失函数,使模型学习到输入数据与输出数据之间的映射关系。
2.模型好坏的评价指标:精确度和准确率
3.明确占用的显存和算力要求
定义神经网络
modle.py文件定义了一个简单的卷积神经网络(CNN),用于处理图像分类任务
代码如下:
import torch
import torch.nn as nn
import torch.nn.functional as F
num_classes = 10 # 图片的类别数
class Model(nn.Module):
def __init__(self):
super().__init__()
# 特征提取网络
self.conv1 = nn.Conv2d(1, 32, kernel_size=3) # 输入图像的通道数、输出图像的通道数、卷积核大小 (RGB图像的输入通道数为3)
self.pool1 = nn.MaxPool2d(2) # 设置池化层,池化核大小为2*2
self.conv2 = nn.Conv2d(32, 64, kernel_size=3) # 第二层卷积,卷积核大小为3*3
self.pool2 = nn.MaxPool2d(2)
# 分类网络
self.fc1 = nn.Linear(1600,64)
self.fc2 = nn.Linear(64,num_classes)
# 前向传播
def forward(self,x):
x = self.pool1(F.relu(self.conv1(x)))
x = self.pool2(F.relu(self.conv2(x)))
x = torch.flatten(x,start_dim=1)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return x
if __name__ == "__main__":
model=Model()
print(model)
分步解析:
1. 导入 PyTorch 相关模块, 定义类别数
import torch
import torch.nn as nn
import torch.nn.functional as F #主模块 torch,神经网络模块 torch.nn 以及函数库 torch.nn.functional
(torch.nn 模块主要包含构建神经网络所需的各种层,而torch.nn.functional 则提供了这些层的激活函数、损失函数和实用工具函数)
num_classes = 10 #类别数为 10,因为 此手写数据集MNIST包含 10 个类别(0 到 9 的数字)
2.定义模型类,并初始化模型
class Model(nn.Module):
def __init__(self):
super().__init__()
3.构建网络层进行特征提取
self.conv1 = nn.Conv2d(1, 32, kernel_size=3) # 输入图像的通道数、输出图像的通道数、卷积核大小 (RGB图像的输入通道数为3)
self.pool1 = nn.MaxPool2d(2) # 设置池化层,池化核大小为2*2
self.conv2 = nn.Conv2d(32, 64, kernel_size=3) # 第二层卷积,卷积核大小为3*3
self.pool2 = nn.MaxPool2d(2)
第一个卷积层 (
self.conv1
):对于灰度图像(如 此MNIST 数据集中的图像)输入为单通道1,32
表示输出通道数(也就是卷积层将产生 32 个特征图),卷积核的大小是 3x3 像素第一个池化层 (
self.pool1
):定义了一个最大池化层,降低特征图的空间尺寸使减少参数数量和计算量,池化窗口的大小是 2x2 像素,并且步长默认也是 2,这意味着池化窗口每次移动 2 个像素第二个卷积层 (
self.conv2
):进一步提取更高级的特征。
- 输入通道数现在是
32
,与第一个卷积层的输出通道数相匹配。- 输出通道数是
64
,意味着这个卷积层将产生 64 个特征图。第二个池化层 (
self.pool2
):与第一个池化层类似
# 分类网络
self.fc1 = nn.Linear(1600,64)
self.fc2 = nn.Linear(64,num_classes)
第一个全连接层将卷积层的输出映射到 64 维空间,第二个全连接层将 64 维特征映射到 10 个类别上。
定义了一个向前函数传播函数forward,它接收输入 x
并依次通过网络层
def forward(self,x):
#输入数据 x 首先通过第一个卷积层 self.conv1并通过 F.relu 函数应用 ReLU激活函数(增加非线性,帮网络学习更复杂特征)接着被送入第一个池化层 self.pool1
x = self.pool1(F.relu(self.conv1(x)))
#同上
x = self.pool2(F.relu(self.conv2(x)))
#将多维的特征图展平成一维
x = torch.flatten(x,start_dim=1)
#被送入第一个全连接层
x = F.relu(self.fc1(x))
#通过第二个全连接层(输出层),输出尺寸与任务的类别数相同
x = self.fc2(x)
这些层在神经网络中通常以“卷积 -> 激活函数 -> 池化”的顺序重复出现。卷积层负责提取特征,激活函数(通常使用 ReLU)引入非线性,而池化层则负责降低特征的空间尺寸,同时增加对图像位移的不变性。
4.测试模型,创建了 Model
的实例并打印,显示模型的架构
if __name__ == "__main__":
model = Model()
print(model)
训练流程
PyTorch 训练流程 :数据加载、模型定义、训练、评估和结果可视化等
代码如下:
import torch
#torch.cuda.is_available()
#print(torch.cuda.is_available())
# nvidia-smi
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(torch.__version__)
print(device)
import torchvision
# 训练集数据
#8000
train_ds = torchvision.datasets.MNIST('data',
train=True,
transform=torchvision.transforms.ToTensor(),
download=True)
# 测试集数据
#2000
test_ds = torchvision.datasets.MNIST('data',
train=False,
transform=torchvision.transforms.ToTensor(),
download=True)
batch_size = 32 # 每批加载样本的大小
# 加载训练集数据
train_dl = torch.utils.data.DataLoader(train_ds,
batch_size=batch_size,
shuffle=True) # 每个epoch重新排列数据
# 加载测试集数据
test_dl = torch.utils.data.DataLoader(test_ds,
batch_size=batch_size)
imgs,labels = next(iter(train_dl))
print(imgs.shape) # 得到结果是 torch.Size([32,1,28,28])
import torch.nn as nn
from model2 import Model
# 打印并加载模型
model = Model().to(device)
print(model)
loss_fn = nn.CrossEntropyLoss() # 创建损失函数
learn_rate = 1e-1 # 学习率
opt = torch.optim.SGD(model.parameters(), lr=learn_rate)
'''
1、optimizer.zero_grad() 函数会遍历模型的所有参数,通过内置方法截断反向传播的梯度流,再将每个参数的梯度值设为0,即上次的梯度记录会被清空。
2、loss.backward() Pytorch的反向传播(即:tensor.backward())是通过autograd包来实现的,autograd包会根据tensor进行过的数学运算来自动计算其对应的梯度。
3、optimizer.step() step() 函数的作用是执行一次优化步骤,通过梯度下降法来更新参数的值。因为梯度下降是基于梯度的,所以在执行optimizer.step() 函数前应先指向那个loss.backward()函数来计算梯度。
'''
# 训练循环
print('准备进入----训练集里面')
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset) # 训练集的大小,一共60000张图片
num_batches = len(dataloader) # 批次数目,1875(60000/32)
train_loss, train_acc = 0, 0 # 初始化训练损失和正确率
for X, y in dataloader: # 获取图片及其标签
X, y = X.to(device), y.to(device)
# 计算预测误差
pred = model(X) # 网络输出
loss = loss_fn(pred, y) # 计算网络输出和真实值之间的差距,targets为真实值,计算二者差值,即为损失。
# 反向传播 (以下三个基本上是固定的)
optimizer.zero_grad() # grade属性归零
loss.backward() # 反向传播
optimizer.step() # 每一步自动更新
# 记录acc与loss
train_acc += (pred.argmax(1) == y).type(
torch.float).sum().item() # 表示计算预测正确的样本数量,并将其作为一个标量值返回。这通常用于评估分类模型的准确率或计算分类问题的正确预测数量。
'''
pred.argmax(1)返回数组pred在第一个轴(即行)上最大值所在的索引。这通常用于多分类问题中,其中pred是一个包含预测概率的二维数组,每行表示一个样本的预测概率分布。
pred.argmax(1) == y是一个布尔值,其中等号是否成立代表对应样本的预测是否正确。(True表示正确,False表示错误)
.type(torch.float)是将布尔数组的数据类型转换为浮点数类型,即将True转换为1.0;将False转换为0.0
.sum() 是对数组中的元素进行求和,计算出预测正确的样本数量。
.item() 将求和结果转换为标量值,以便在Python中使用或打印。
'''
train_loss += loss.item()
train_acc /= size
train_loss /= num_batches
return train_acc, train_loss
def test(dataloader, model, loss_fn):
size = len(dataloader.dataset) # 测试集的大小,一共10000张图片
num_batches = len(dataloader) # 批次数目313 (10000/32=321.5 向上取整)
test_loss, test_acc = 0, 0
# 当不进行训练时,停止梯度更新,节省计算内存消耗
with torch.no_grad():
for imgs, target in dataloader:
imgs, target = imgs.to(device), target.to(device)
# 计算loss
target_pred = model(imgs)
loss = loss_fn(target_pred, target)
test_loss += loss.item()
test_acc += (target_pred.argmax(1) == target).type(torch.float).sum().item()
test_acc /= size
test_loss /= num_batches
return test_acc, test_loss
if __name__ == "__main__":
epochs = 5
train_loss = []
train_acc = []
test_loss = []
test_acc = []
for epoch in range(epochs): # epoch 索引值
model.train() # 启用Batch Normalization和Dropout
'''
如果模型中有BN(Batch Normalization)和Dropout ,需要在训练时添加model.train() 。 model.train() 是保证BN层能够用到每一批数据的均值和方差。
对于Dropout ,model.train() 是随机取一部分网络连接来训练更新参数。
'''
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
model.eval() # 不启用Batch Normalization 和Dropout
'''
如果模型中有BN(Batch Normalization)和Dropout ,需要在测试时添加model.eval() . model.eval() 是保证BN层能够用全部训练数据的均值和方差,
即:测试过程中要保证BN层的均值和方差不变。对于Dropout, model.eval() 是利用到了所有网络连接,即:不进行随机舍弃神经元。
训练完train样本后,生成的模型model要用来测试样本。在model(test)之前,需要加上model.eval(),否则的话,有输入数据,即使不训练,它也会改变权值。
这是model中还有BN层和Dropout所带来的性质。
'''
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
train_acc.append(epoch_train_acc)
train_loss.append(epoch_train_loss)
test_acc.append(epoch_test_acc)
test_loss.append(epoch_test_loss)
template = 'Epoch:{:2d}, Train_acc:{:.1f}%, Train_loss:{:.3f},Test_acc:{:.1f}%,Test_loss:{:.3f}'
print(template.format(epoch + 1, epoch_train_acc * 100, epoch_train_loss, epoch_test_acc * 100, epoch_test_loss))
print('Done')
import matplotlib.pyplot as plt
# plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rc('font', family='PingFang HK')
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.dpi'] = 100 # 分辨率
epochs_range = range(epochs)
plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_acc, label='Training Accuracy')
plt.plot(epochs_range, test_acc, label='Test Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Train Loss')
plt.plot(epochs_range, test_loss, label="Test Loss")
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()解释代码
如果没有安画图工具插件可以去掉Done后面的结果可视化部分。
分步解读:
1. 导入必要的库和设置设备
import torch
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
#检查 CUDA 是否可用,不可用就使用cpu设备
import torchvision
输出print(torch.__version__)
print(device)检查已经设置好的设备
2. 加载并创建数据集
train_ds = torchvision.datasets.MNIST('data', train=True, ... )
test_ds = torchvision.datasets.MNIST('data', train=False, ... )
#加载了 MNIST 训练集和测试集数据,并使用 ToTensor 转换将 PIL 图像转换为 torch.FloatTensor 类型
train_dl = torch.utils.data.DataLoader(train_ds, ... )
test_dl = torch.utils.data.DataLoader(test_ds, ... )
#用DataLoader 创建了两个数据加载器
3.模型定义和初始化
从 model2
模块导入 Model
类并创建模型实例,将模型移动到设备上
from model2 import Model
model = Model().to(device)
4.定义损失函数和优化器
损失函数和优化器是训练神经网络的两个基本组件。损失函数评估模型的预测与真实标签之间的差异,优化器则利用这些信息来更新模型的参数权重,以减少损失。
loss_fn = nn.CrossEntropyLoss()
#用 nn.CrossEntropyLoss() 创建了一个交叉熵损失函数的实例
learn_rate = 1e-1
#设置了优化器的学习率,这里设置为 0.1
opt = torch.optim.SGD(model.parameters(), lr=learn_rate)
#创建了一个随机梯度下降(SGD)优化器的实例
交叉熵损失是分类问题中最常用的损失函数之一,特别是在多分类问题中。它衡量的是模型预测的概率分布与真实标签的概率分布之间的差异。值越小越好。
学习率是优化算法中用于调整模型权重的步长的超参数。较小的学习率可以使训练过程更稳定,但可能导致训练速度较慢;较大的学习率可能会加快收敛速度,但也可能导致训练过程不稳定,甚至发散。
SGD通过计算梯度并更新每个参数来最小化损失函数。
model.parameters()
返回模型中所有需要优化的参数。lr=learn_rate
设置了优化器的学习率0.1
5.定义训练和测试函数
定义了 train
和 test
函数,执行模型的训练和测试。
def train(dataloader, model, loss_fn, optimizer):
...
def test(dataloader, model, loss_fn):
...
6.进入训练循环
在主函数中,设置了训练的轮数(epochs),在每个 epoch 中调用 train
和 test
函数,记录训练和测试的准确率和损失
if __name__ == "__main__":
...
for epoch in range(epochs):
model.train() # 启用Batch Normalization和Dropout,model.train() 是保证BN层能够用到每一批数据的均值和方差。
...
epoch_train_acc, epoch_train_loss = train(train_dl, model, loss_fn, opt)
model.eval() # 不启用Batch Normalization 和Dropout,model.eval() 是保证BN层能够用全部训练数据的均值和方差。
...
epoch_test_acc, epoch_test_loss = test(test_dl, model, loss_fn)
...
注意:训练完train样本后,生成的模型model要用来测试样本,在model(test)之前,需要加上model.eval()来固定训练数据。否则的话,有输入数据,即使不训练,它也会改变权值。
7. 结果可视化(可去掉这部分)
使用 Matplotlib 库绘制了训练和测试的准确率和损失曲线。
import matplotlib.pyplot as plt
...
plt.show()
加载预训练 的PyTorch 模型
整个流程展示了如何加载预训练模型、处理图像数据以及使用模型进行预测
代码如下:
import torch
from model2 import Model
import torchvision.transforms as transforms
from PIL import Image
def load_model(model_path):
model = Model()
model.load_state_dict(torch.load(model_path))
model.eval()
return model
#前处理
def preprocess_image(image_path):
image = Image.open(image_path).convert('L')
transform = transforms.Compose([
transforms.Resize((28, 28)),
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
return transform(image).unsqueeze(0)
def predict(model, input_tensor):
with torch.no_grad():
output = model(input_tensor)
_, predicted = torch.max(output.data, 1)
return predicted.item()
if __name__ == "__main__":
model_path = r'D:\VSC\手写\best_model.pth'
image_path = r'D:\VSC\手写\20240809160320.jpg'
model = load_model(model_path)
input_tensor = preprocess_image(image_path)
prediction = predict(model, input_tensor)
print(f"Predicted digit: {prediction}")
分步解析:
1.导入必要的库
import torch
from model2 import Model
import torchvision.transforms as transforms#图像变换库 transforms
from PIL import Image #图像处理库 PIL
2.定义加载模型的函数
首先实例化
Model
类,然后使用torch.load
加载保存的模型状态字典,并调用model.eval()
将模型设置为测试模式
def load_model(model_path):
model = Model()
model.load_state_dict(torch.load(model_path))
model.eval()
return model
3.定义图像预处理函数
preprocess_image
函数用于读取图像文件,将其转换为灰度('L' 模式),调整大小为 28x28 像素(假设模型针对 28x28 输入进行了训练),然后将图像转换为张量,并进行归一化处理。
def preprocess_image(image_path):
image = Image.open(image_path).convert('L')
transform = transforms.Compose([
transforms.Resize((28, 28)),
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
return transform(image).unsqueeze(0)
最后,使用 unsqueeze(0)
增加一个批次维度,因为模型预期批量输入。
4.定义预测函数
使用 torch.no_grad()
确保在预测时不计算梯度,减少内存消耗并提高性能。然后,它将输入张量传递给模型,获取输出,并使用 torch.max
找到最大概率值对应的索引,该索引即为预测的类别。
def predict(model, input_tensor):
with torch.no_grad():
output = model(input_tensor)
_, predicted = torch.max(output.data, 1)
return predicted.item()
5.主函数
if __name__ == "__main__":
model_path = r'D:\VSC\手写\best_model.pth'
image_path = r'D:\VSC\手写\20240809160320.jpg'
#定义模型和图像的路径
model = load_model(model_path)#加载模型
input_tensor = preprocess_image(image_path)#对图像进行预处理
prediction = predict(model, input_tensor)#进行预测并打印
print(f"Predicted digit: {prediction}")
运行
最终输出结果如下图: