写在前面的话:
本篇博客的内容是由学校老师教授后的个人总结(选修课),需要python基础,需要安装pytorch,PIL,cv2等包,这些当然都可以在csdn上找到教学。由于还未系统学习深度学习等课程,以下内容如有出错,恳请各位大佬批评指正。当然,这也代表你即使是个小白,也可以按部就班的和我一起尝试本次实践。
首先,我们要讲一下本次实践的内容:
1.获取数据集
2.读取数据集
3.搭建神经网络模型
4.定义损失函数
5.定义优化器来优化参数
6.模型训练
7.模型检测
但我并非由上到下顺序讲解,而是根据之前老师的讲解顺序来,也许老师考虑了我们还是小白。
第一课:学习搭建全连接神经网络
全连接神经网络(Fully - Connected Neural Network,FCN)是神经网络中较为基础的一种结构。在这种网络中,相邻两层的神经元之间全部相互连接,即前一层的每个神经元都与后一层的每个神经元有连接权重。
以上是官方定义,下面插图帮助理解
在本次实践中,输入层是从数字图片中所读取的信息,而每一层,神经元先对输入进行加权求和
由输入到输出的过程就是前向传播,它的结果表示输入数据经过我们搭建的模型所得到的结果,在本次实践中,我们输入由0到9的图片的读取参数,最后得到它识别的结果。那么你现在必定有一个问题,就是最后的结果怎么能提高正确率。其实这主要是由w,b两个参数决定,而怎么得到这两个参数的更优值来使得结果准确率较高,之后的课程会讲,此处我们只要理解什么是全连接神经网络和前向传播即可。
我们首先要了解来两个函数nn.Linear()和nn.ReLU(),nn是torch中的模块,非常有用。
nn.Linear
是 PyTorch 中用于实现全连接层(也叫线性层)的模块,用于线性变换,此处需要一点点的矩阵乘法的知识。就是我们输入的是矩阵(或列向量),要乘以一个权重矩阵w(不是一个数)
我们知道了nn.Linear是用于构建全连接神经网络,那nn.Relu是什么?答案是激活函数。下面是关于激活函数的作用:激活函数是神经网络中不可或缺的组成部分,它的核心作用是为网络引入非线性特性,从而使神经网络能够学习复杂的函数映射。
我的理解就是如果没有激活函数,那么即使再多层也只是线性函数,就是y=kx+b嵌套再多层也是一次函数(类比)
它本身很简单:把负数变成0,正数不变
那么我们先正式写代码:
import torch
from torch import nn#导入库
下面我们创建一个类,方便以后导入和创建实例。
#神经网络搭建部分
class MNIST_Net(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = nn.Linear(784, 256)
self.layer2 = nn.Linear(256, 256)
self.layer3 = nn.Linear(256, 10)
self.relu = nn.ReLU()
def forward(self, x):
x = x.view(-1, 784)
x = self.layer1(x)
x = self.relu(x)
x = self.layer2(x)
x = self.relu(x)
x = self.layer3(x)
return x
首先我们要注意到我们创建的MNIST_Net继承了nn.Module,这里面有很多常用函数,这里有ai的解释:nn.Module
是所有神经网络模块的基类,也是构建自定义神经网络的核心工具。通过继承 nn.Module
,可以方便地组织和管理神经网络的层、参数以及前向传播逻辑。此处我们主要是利用它的前向传播。
我们可以看到这个类有两部分,一个是必写的__init__函数,类似于构造函数,其中有三层和一个激活函数作为成员变量,在forward中会用到,你也可以多写几层自己试一试,但后面好像还是要改回来,有我们后面的代码有关。
x = x.view(-1, 784)解释一下这一句,这是把x变成784列。这是因为输入层要求784,必须对应,-1表示前面由总数/784决定行数。
代码尝试运行:
if __name__=='__main__':
net=MNIST_Net()
x=torch.ones(28,28)
y=net(x)
print(y)
x=torch.ones(28,28)表示一个28*28的全为1的二维张量,理解为矩阵应该也行。x =torch.randn(1, 784),不过这是生成随机的数了。由于此时我们没有训练,此处的w和b都是随机的,结果也随机。
第二课:创建和读取数据集
首先,我们来学习如何读取一张图片,可以用PIL中的Image.open(),或者cv2中的cv2.imread,现在你可以下载我的资源里的压缩包,然后解压。
注意:必须解压,就是一会要复制的路径里不能有压缩包
示例:
im=cv2.imread(r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST\0\542.jpg',0)
print(im)
结果:
此处你不用和我用一致的地址,找对应图片,复制上面的地址即可。
ok,到这里你就知道了怎么读取一张图,但是这个压缩包有几万张图片,那么你不可能像上面一样一张一张地读取,下面我们来介绍怎么一次读取一个文件里的所有图片。
代码:
data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST'
classes = os.listdir(data_root)
for c in classes:
img_names = os.listdir(os.path.join(data_root, c))
for img_name in img_names:
img_num += 1
test_img = Image.open(os.path.join(data_root, c, img_name))
# 用 PIL 打开图片
解释: data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST'这是我们要访问的文件夹,此处r是因为\是转义字符,会报错,必须加r或者用/,又或者\\。
此时data_root数据类型是字符串,os.listdir(data_root),这个os是不用下载自带的包。这个方法作用是把文件夹中所有子文件夹名称存入一个列表并返回。
os.path.join(data_root, c)是把路径合并,此时可以进一步把其中的子文件名称全部读取,然后再合并最后一次。所以我们通过for循环遍历和这两个方法实现了我们的需求。
但是,老师接下来说我们这样写还是略显麻烦,最好能写成一个类方便以后调用。并且还有
from torch.utils.data import Dataset from torch.utils.data import DataLoader
这两个库可以使用,这两个库在后面有用
import torch
from torch import nn
from PIL import Image
import os
from MNIST_Net import MNIST_Net
import torchvision.transforms as transforms
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
#创建一个类用于读取一个路径下的所有文件,并存储信息
class MNIST_Data(Dataset):
def __init__(self, data_root):
self.data_root = data_root
self.dataset = []
classes = os.listdir(self.data_root)
for c in classes:
img_names = os.listdir(os.path.join(data_root, c))
for img_name in img_names:
path = os.path.join(data_root, c, img_name)
label = int(c)#类别标签,此前c是字符串
self.dataset.append((path, label))#将数据对(路径,标签)不断加入列表
#分开写是为减小所耗费的存储空间,当存储图片信息时,最好存储路径
def __getitem__(self, idx):
path, label = self.dataset[idx]#读取路径,标签
img = Image.open(path)
my_trans = transforms.ToTensor()
img = my_trans(img)
return img, label
def __len__(self):
return len(self.dataset)#返回文件数
此处我们只用了Dataset后面具体使用时,会有相应的解释。
第三课:测试
现在,我们已经学会读取数据集和搭建模型,我们先跳过剩下训练步骤,用老师给的训练好的文件,测试一下,相关文件也可以在我的资源找到。
代码:
import torch
from torch import nn
from PIL import Image
import os
from MNIST_Net import MNIST_Net
import torchvision.transforms as transforms
if __name__ == '__main__':
net = MNIST_Net()
net.load_state_dict(torch.load('fcn_mnist_ckpt.pt', map_location='cpu'))
data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST'
classes = os.listdir(data_root)
score_all = 0
img_num = 0
for c in classes:
img_names = os.listdir(os.path.join(data_root, c))
for img_name in img_names:
img_num += 1
test_img = Image.open(os.path.join(data_root, c, img_name))
# 用 PIL 打开图片
my_trans = transforms.ToTensor()
test_img = my_trans(test_img)
score = net(test_img)
softmax = nn.Softmax(dim=1)
score = softmax(score)
answer = torch.argmax(score, dim=1)
score_all += float(answer[0].numpy() == int(c))
test_score_all = score_all / img_num
print(f'{c} Test score: {test_score_all:.2f}')
这里我们主要解释这几句,毕竟前面都差不多讲了:
net = MNIST_Net()
net.load_state_dict(torch.load('fcn_mnist_ckpt.pt', map_location='cpu'))
score = net(test_img)
softmax = nn.Softmax(dim=1)
score = softmax(score)
answer = torch.argmax(score, dim=1)
score_all += float(answer[0].numpy() == int(c))
test_score_all = score_all / img_num
print(f'{c} Test score: {test_score_all:.2f}')
net = MNIST_Net()是我们之前搭建的神经网络模型的实例化,net.load_state_dict(torch.load('fcn_mnist_ckpt.pt', map_location='cpu'))这里你肯定会疑惑为啥有这个方法load_state_dict(),不是只写了两个方法吗,别忘了我们继承了nn.Module。它的主要作用就是导入我们需要的参数,这个文件名为fcn_mnist_ckpt.pt,用cpu运行。这里的fcn_mnist_ckpt.pt要放在代码同一文件夹下。
如图:
score = net(test_img)这是模型运行结果,是一列数,共十个,数越大,越说明代表对应图片是它所在位置。其实由于我们已经训练好了模型, softmax = nn.Softmax(dim=1),这一句话理论上可以不用,这里还是解释一下,反正下面要用。
主要就是利用指数函数的非负性,保证每个数对应概率非负,且相加为1。dim=1表示列。
answer = torch.argmax(score, dim=1)这里表示找出每列最大值对应的索引,这就是我们识别的图片结果,score_all += float(answer[0].numpy() == int(c))表示识别正确加一分。
test_score_all = score_all / img_num表示识别准确率。
结果:
我们再使用之前写好的类MNIST_Data来让我们的代码更加简洁:
import torch
from torch import nn
from MNIST_Data import MNIST_Data
from PIL import Image
import os
from MNIST_Net import MNIST_Net
import torchvision.transforms as transforms
from torch.utils.data import Dataset
from torch.utils.data import DataLoader
#创建一个类用于读取一个路径下的所有文件,并存储信息
class MNIST_Data(Dataset):
def __init__(self, data_root):
self.data_root = data_root
self.dataset = []
classes = os.listdir(self.data_root)
for c in classes:
img_names = os.listdir(os.path.join(data_root, c))
for img_name in img_names:
path = os.path.join(data_root, c, img_name)
label = int(c)#类别标签,此前c是字符串
self.dataset.append((path, label))#将数据对(路径,标签)不断加入列表
def __getitem__(self, idx):
path, label = self.dataset[idx]#读取路径,标签
img = Image.open(path)
my_trans = transforms.ToTensor()
img = my_trans(img)
return img, label
def __len__(self):
return len(self.dataset)#返回文件数
if __name__ == '__main__':
test_data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST'
#构建测试集
test_data = MNIST_Data(test_data_root)
test_dataloader = DataLoader(dataset=test_data, batch_size=1, shuffle=False)
net = MNIST_Net()
net.load_state_dict(torch.load('fcn_mnist_ckpt.pt', map_location='cpu'))
#从文件fcn_mnist_ckpt.pt加载模型权重,
# 将权重映射到 CPU 设备(无论模型原本在哪个设备上训练),将加载的权重应
#用到当前的net模型实例中
test_score_all = 0
for img, label in test_dataloader:
score = net(img)
softmax = nn.Softmax(dim=1)
score = softmax(score)
answer = torch.argmax(score, dim=1)
score_batch = torch.eq(answer, label).float().sum()#一组之和
test_score_all = test_score_all + score_batch
test_score_all = test_score_all / len(test_data)#整个数据集的准确率
print('Test score: %.2f'%test_score_all)
这里主要解释一下Dataset,DataLoader的作用
通过对比之前我们用两个for循环写的代码,这里最大的区别在于将数据集以数据对的形式进行了存储,方便管理,继承Dataset是因为方法与DataLoader有关,DataLoader
是一个迭代器,用于批量加载和处理Dataset
中的数据。这里
test_data = MNIST_Data(test_data_root)是将测试点数据集构建完成,
DataLoader(dataset=test_data, batch_size=1, shuffle=False)是表示对应数据集以批次为1,不打乱构造一个迭代器,然后我们就可以从这个迭代器里直接获取图像对应的张量和标签。再把张量像之前一样处理即可。
运行结果:
第四课:自己训练模型,优化参数
我们先来了解一下反向传播,又称后向传播,它对参数优化,减小误差很有帮助。顾名思义,就是前向传播的反过来。
反向传播(Backpropagation)是一种在人工神经网络中用于计算梯度的算法,主要用于训练神经网络,以下是关于它的详细介绍:
- 反向传播算法基于链式法则,将误差从输出层反向传播到输入层,以计算每个神经元的权重梯度。具体来说,它首先计算输出层的误差,然后根据误差对输出层的权重和偏置进行调整。接着,将误差反向传播到隐藏层,计算隐藏层的误差,并对隐藏层的权重和偏置进行调整。这个过程不断重复,直到误差传播到输入层。
按照我的理解就是一个多元函数原本是以x1,x1。。。xn为变量,然后计算结果发现有误差,再以w1,w2。。。wn,b1,b2。。。bn为主元,进行微分,向使损失函数降低的方向前进。(梯度是方向函数max)
这里多讲一下:ai的看法
你的理解基本正确。
以损失函数(比如均方误差等)作为衡量误差大小的指标,损失函数是关于权重 w 和偏置 b 的函数。根据链式法则对损失函数关于权重 w 和偏置 b 求微分(也就是计算梯度),然后沿着梯度的反方向(使得损失函数降低的方向)来更新权重和偏置,以此来不断优化神经网络的性能,使得网络的输出尽可能地接近真实值。
这里我们提到了误差,又称损失,那么我们要怎么计算损失呢,需要一个损失函数。这里采用的是交叉熵损失函数。
代码实现:
loss_ce = nn.CrossEntropyLoss()
手动代码:
def cross_entropy(y_hat, y):
return -torch.log(y_hat[range(len(y_hat)), y])
有了损失函数,下一步是优化器用来优化参数
代码:
# 定义优化器
opt = torch.optim.SGD(net.parameters(), lr=0.01) # learning rate 学习率
学习率(Learning Rate) 是一个至关重要的超参数,它控制着模型参数(权重和偏置)在每次迭代中根据梯度更新的步长大小,这是一个比较难的地方,暂时不需要掌握,理解成更新速度有关即可,但不能太大太小。
运用优化器
opt.zero_grad() # 清空梯度
loss.backward() # 反向传播
opt.step() # 进行一步更新
手动代码:
def sgd(params, lr, batch_size):
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
其实这里一样就是上面的三句注解,with torch.no_grad()表示梯度清零,因为pytorch默认梯度累加,要避免之前计算的影响就要梯度清零。下面for循环就是参数更新:
aram.grad
: 当前批次计算得到的梯度。/ batch_size
: 归一化处理,因为 PyTorch 中损失函数默认对一个批次内的样本取平均,而梯度通常是累积的,所以需要除以批量大小。lr * ...
: 缩放梯度,控制更新步长。param -= ...
: 原地更新参数,向梯度反方向移动。
所以通过以上代码我们就可以完成训练
'''准备大量数据(训练测试数据),构建模型,训练模型'''
import os
import torch
import torch.nn as nn
from MNIST_Net import MNIST_Net
from matplotlib import pyplot as plt
from MNIST_Data import MNIST_Data
from torch.utils.data import Dataset, DataLoader
os.environ["KMP_DUPLICATE_LIB_OK"]= 'TRUE'
if __name__ == '__main__':
# 训练集目录位置
train_data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TRAIN'
# 构建训练集dataloader
train_data = MNIST_Data(train_data_root)
train_dataloader = DataLoader(dataset=train_data, batch_size=8, shuffle=True)
# 测试集目录位置
test_data_root = r'C:\Users\33176\Downloads\MNIST_IMG\MNIST_IMG\TEST'
# 构建测试集dataloader
test_data = MNIST_Data(test_data_root)
test_dataloader = DataLoader(dataset=test_data, batch_size=1, shuffle=False)
# 从MNIST_Net类中实例化一个网络, 命名为 net
net = MNIST_Net()
# 定义损失函数
loss_ce = nn.CrossEntropyLoss()
# 定义优化器
opt = torch.optim.SGD(net.parameters(), lr=0.01) # learning rate 学习率
epoch = 3 # 整个训练数据集 遍历的 轮数
# 初始化两个列表,报出每轮的loss和score,以便训练结束时用plot显示
train_loss_epoch = []
test_score_epoch = []
# 开始训练
for e in range(epoch):
for img, label in train_dataloader: # 从dataloader中遍历图片和标签
score = net(img) # 预测结果
loss = loss_ce(score, label) # 预测值和标签计算损失
opt.zero_grad() # 清空梯度
loss.backward() # 反向传播
opt.step() # 进行一步更新
那么我们需要检测我们的训练结果:
for e in range(epoch):
loss_all = 0
for img, label in train_dataloader: # 从dataloader中遍历图片和标签
score = net(img) # 预测结果
loss = loss_ce(score, label) # 预测值和标签计算损失
opt.zero_grad() # 清空梯度
loss.backward() # 反向传播
opt.step() # 进行一步更新
loss_all += loss
train_loss_epoch.append(loss_all.detach().numpy() / len(train_dataloader)) # 保存本轮loss
if not os.path.exists('train_checkpoint'):
os.mkdir('train_checkpoint')
torch.save(net.state_dict(), 'train_checkpoint/mnist_ckpt_%d.pth' % e) # 保存本轮权重checkpoint
test_score_all = 0
for img, label in test_dataloader:
score = net(img) # 输入测试图像,此时net会自动调用执行forward函数的过程,返回网络的输出结果
softmax = nn.Softmax(dim=1)
score = softmax(score) # 利用 softmax 将 网络输出结果转化为是分类的概率向量
answer = torch.argmax(score, dim=1) # 利用 argmax,找到score中最大值的位置
score_batch = torch.eq(answer, label).float().sum() # 计算结果是否和类别标签一致
test_score_all = test_score_all + score_batch
test_score_all = test_score_all / len(test_data) # 计算平均值
test_score_epoch.append(test_score_all) # 保存本轮score
print('Epoch %d, Train loss: %.2f, Test score: %.2f' \
% (e, loss_all / len(train_dataloader), test_score_all))
if not os.path.exists('train_checkpoint'):
os.mkdir('train_checkpoint')
torch.save(net.state_dict(), 'train_checkpoint/mnist_ckpt_%d.pth' % e) # 保存本轮权重checkpoint
这几句解释一下,就是创建了一个文件夹以二进制的形式用来保存你的 每轮循环完后的参数,这样你就有了我之前给你的参数了。
注意:
opt = torch.optim.SGD(net.parameters(), lr=0.01)
这句代码建立了net中参数和优化器的联系,所以我们接下来每一轮都是用更新过的参数来检验
运行结果:
可以看到损失逐渐降低,准确率逐步提高。
最后的代码实用来画图可视化的,可以运行看看。
感觉自己初学者水平还是不够,有很多地方不太正确还望见谅。
最后,说一下,就是你可以试着要ai帮你写一段代码来用于你进行手写数字图片,在用程序识别,可以再不懂的人面前装一装(狗头)