目录
本文内容参考了多篇相关文章,旨在进行学习与总结。如涉及版权问题,请作者及时联系删除,本人将不胜感激并深表歉意。
学习资料如下:
1、
本文旨在建立一个简单的数字识别神经网络,并介绍相关原理,附带实验过程,强烈建议先看完资料一中的内容。
原理简介:
单层神经网络
首先讲一下简单的标准神经网络,标准神经网络格式如下:
这是一种单层神经网络(我们定义进行计算层的数量来定义神经网络层数,这里计算层只有一个,所有是单层神经网络),设输入为 x1 、 x2 、x3,权值为 w1 、w2 、w3
这里我们假设激活函数为阶跃函数:
,则很容易得到输入值经过该网络计算得到的结果:
如果 < 0,则值为 0 ,否则则为 1
这种单层神经网络也被称为感知机,能够解决一些简单的线性分类问题
但这是没加偏置项的情况,当输入的 xi 都为 0 时,不管权重多大,最后的结果一定是 0 。这不一定符合我们的任务需求,比如在做房价预测时,假设当面积、房间数等特征都为 0 时,理论上房子不存在,但也许你希望预测一个“基础成本”(比如土地成本),这时输出就不该是 0。
可以理解为:没有偏置项时,拟合的函数是经过原点的,而有了偏置项,允许函数上下移动,更能满足现实世界的复杂需求。
加上偏置层后,公式变为:
这个偏置项也算是一个节点,称为偏置节点,只是输入和权重固定,所以还是可以表达为之前的公式:
BP 神经网络
上述结构为单层神经网络,能够解决简单的分类问题,但在处理线性不可分问题时,无法通过简单训练实现收敛,或者无法学习复杂的非线性关系。
例如:在养殖场随机抓两只鸡,判断这两只鸡是否有诞生小鸡的可能。这是一个典型的异或问题,只有当两只鸡的性别不同,即(0,1)、(1,0)两种情况时,才有诞生小鸡的可能,(0,0)和(1,1)都没有一点可能性。所以需要将能诞生小鸡的分为一类,不能诞生小鸡的分为一类,但你会发现,无论你画哪种直线,都无法实现上面的分类法则,如图所示:
但多层神经网络能做到这一点,比如 BP 神经网络,BP 神经网络等于叠加多个感知机来实现非线性可分,其在输入层和输出层中间添加了隐藏层,这隐藏层可以有多个层,每个层都有一个激活函数,每个层用来解决一种问题,逻辑与、逻辑并、逻辑交等等,从而具备了学习非线性关系的能力。理论上来说,只要 3 层,就能拟合任何非线性函数了。
BP 神经网络是一种前馈型神经网络,整体训练流程如下:
1、模型初始化,人为设置权重 wi 的初始值
2、准备、处理数据,对于输入数据,这里是 28×28 的单通道图像,即输入层应有 28×28 个 x,一般为了数据更加友好,会对数据进行归一化,这里即除以 255。
3、将数据输入,计算得到估计值
4、将模型求得估计值 与实际值 y 比较,若差值达到某个阈值,则完成训练,否则,以目标的负梯度方向对参数进行调整,这里使用均方根误差:
再将算出的权重调整值加到每一层的对应权重中:
5、直到满足终止条件,否则继续对权重 wi 进行优化,重复步骤 3-5
实例
数据获取
代码:
# 这个程序的功能会先将MNIST数据下载下来,然后再保存为.png的格式。
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor
from torchvision.transforms import ToPILImage
import os
import secrets
# 下载数据
train_data = MNIST(root='./data', train=True, download=True, transform=ToTensor())
test_data = MNIST(root='./data', train=False, download=True, transform=ToTensor())
# ToPILImage 张量格式 转换为 PIL 图像格式
train_data = [(ToPILImage()(img), label) for img, label in train_data]
test_data = [(ToPILImage()(img), label) for img, label in test_data]
# 保存下来
def save_images(dataset, folder_name):
root_dir = os.path.join('./mnist_images', folder_name)
os.makedirs(root_dir, exist_ok=True)
# 这10个子文件夹保存每种数字的数据
for i in range(len(dataset)):
img, label = dataset[i]
label_dir = os.path.join(root_dir, str(label))
os.makedirs(label_dir, exist_ok=True)
# 随机生成字符用于图片名
random_filename = secrets.token_hex(8) + '.png'
img.save(os.path.join(label_dir, random_filename))
save_images(train_data, 'train')
save_images(test_data, 'test')
上述代码用于加载数字图片数据集和标注集,并保存到本地,标注集包括0-9,共十个数字
下载后如图所示:
下载需要 pytorch 环境,需要下载 GPU 版 pytorch 环境的可以参考这一篇:
定义网络
网络结构:
代码:
使用 pytorch 定义网络,并打印整个过程中的结构变换:
import torch
from torch import nn
# 定义神经网络Network
class Network(nn.Module):
def __init__(self):
# 调用父类 nn.Module 的 __init__ 方法
super().__init__()
# 线性层1,输入层和隐藏层之间的线性层
self.layer1 = nn.Linear(784, 256)
# 线性层2,隐藏层和输出层之间的线性层
self.layer2 = nn.Linear(256, 10)
# 在前向传播,forward函数中,输入为图像x
def forward(self, x):
# -1 表示自动计算 batch size,使用view函数,将x展平
x = x.view(-1, 28 * 28)
x = self.layer1(x) # 将x输入至layer1,自动初始化权重
# 引入非线性 relu 激活函数,让神经网络可以学习更复杂的模式。
x = torch.relu(x)
return self.layer2(x) # 输入至layer2计算结果
#手动的遍历模型中的各个结构,并计算可以训练的参数
def print_parameters(model):
cnt = 0
for name, layer in model.named_children(): #遍历每一层
# 打印层的名称和该层中包含的可训练参数
print(f"layer({name}) parameters:")
for p in layer.parameters():
# print("p:",p)
print(f'\t {p.shape} has {p.numel()} parameters')
cnt += p.numel() #将参数数量累加至cnt
#最后打印模型总参数数量
print('The model has %d trainable parameters\n' % (cnt))
#打印输入张量x经过每一层时的维度变化情况
def print_forward(model, x):
print(f"x: {x.shape}") # x从一个5*28*28的输入张量
x = x.view(-1, 28 * 28) # 经过view函数,变成了一个5*784的张量
print(f"after view: {x.shape}")
x = model.layer1(x) #经过第1个线性层,得到5*256的张量
print(f"after layer1: {x.shape}")
x = torch.relu(x) #经过relu函数,没有变化
print(f"after relu: {x.shape}")
x = model.layer2(x) #经过第2个线性层,得到一个5*10的结果
print(f"after layer2: {x.shape}")
if __name__ == '__main__':
model = Network() #定义一个Network模型
print(model) #将其打印,观察打印结果可以了解模型的结构
print("")
print_parameters(model) #将模型的参数打印出来
#打印输入张量x经过每一层维度的变化情况
x = torch.zeros([5, 28, 28])
print_forward(model, x)
输入的图像是大小为 28×28 的灰度图像,每次训练时输入 5 张图像作为一个批次。为了适配全连接层的输入要求,每张图像会被展平成一个 784 维的一维向量。
在定义好神经网络结构后,模型会自动初始化每一层的权重矩阵和偏置项。输入数据首先经过第一个线性层(nn.Linear(784, 256)
),该层对输入进行线性变换,输出一个形状为 (5, 256)
的张量,表示每张图像被映射到 256 维的特征空间。
随后,该输出会传入第二层线性层(nn.Linear(256, 10)
),进一步进行变换,最终得到一个形状为 (5, 10)
的输出张量,表示每张图像在 10 个类别上的预测得分。
运行后打印模型结构:
Network(
(layer1): Linear(in_features=784, out_features=256, bias=True)
(layer2): Linear(in_features=256, out_features=10, bias=True)
)
layer(layer1) parameters:
torch.Size([256, 784]) has 200704 parameters
torch.Size([256]) has 256 parameters
layer(layer2) parameters:
torch.Size([10, 256]) has 2560 parameters
torch.Size([10]) has 10 parameters
The model has 203530 trainable parameters
x: torch.Size([5, 28, 28])
after view: torch.Size([5, 784])
after layer1: torch.Size([5, 256])
after relu: torch.Size([5, 256])
after layer2: torch.Size([5, 10])
训练模型
import torch
from torch import nn
from torch import optim
from model import Network # 前面定义的模型
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
if __name__ == '__main__':
# 图像的预处理
transform = transforms.Compose([
transforms.Grayscale(num_output_channels=1), # 转换为单通道灰度图
transforms.ToTensor() # 转换为张量并自动归一化
])
# 读入数据集并应用 transform
train_dataset = datasets.ImageFolder(root='./mnist_images/train', transform=transform)
print("train_dataset length: ", len(train_dataset))
# 小批量的数据读入,shuffle=True 打乱数据顺序,防止模型记住顺序
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
print("train_loader length: ", len(train_loader))
model = Network() # 实例化前面定义的模型
# 根据计算得到的梯度,优化器会调整模型参数,使得损失函数值逐渐减小
optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss() # 多分类问题,使用交叉熵损失误差
# 进入模型的迭代循环
for epoch in range(10): # 外层循环,代表了整个训练数据集的遍历次数
# 内存循环使用train_loader,进行小批量的数据读取
for batch_idx, (data, label) in enumerate(train_loader):
# 内层每循环一次,就会进行一次梯度下降算法
# 包括了5个步骤:
output = model(data) # 1.计算神经网络的前向传播结果
loss = criterion(output, label) # 2.计算output和标签label之间的损失loss
loss.backward() # 3.使用backward计算梯度
optimizer.step() # 4.使用optimizer.step更新参数
optimizer.zero_grad() # 5.将梯度清零
# 这5个步骤,是使用pytorch框架训练模型的定式,初学的时候,先记住就可以了
# 每迭代100个小批量,就打印一次模型的损失,观察训练的过程
if batch_idx % 100 == 0:
print(f"Epoch {epoch + 1}/10 "
f"| Batch {batch_idx}/{len(train_loader)} "
f"| Loss: {loss.item():.4f}")
torch.save(model.state_dict(), 'mnist.pth') # 保存模型
将训练集每 64 张图片分为一批,总共有 9800 张图片,分为 152 批训练,然后循环 10 轮,训练完成后模型保存至 mnist.pth
测试模型
from model import Network
from torchvision import transforms
from torchvision import datasets
import torch
if __name__ == '__main__':
transform = transforms.Compose([
transforms.Grayscale(num_output_channels=1),
transforms.ToTensor()
])
# 读取测试数据集
test_dataset = datasets.ImageFolder(root='./mnist_images/test', transform=transform)
print("test_dataset length: ", len(test_dataset))
model = Network() # 定义神经网络模型
model.load_state_dict(torch.load('mnist.pth')) # 加载刚刚训练好的模型文件
right = 0 # 保存正确识别的数量
for i, (x, y) in enumerate(test_dataset):
output = model(x) # 将其中的数据x输入到模型
predict = output.argmax(1).item() # 选择概率最大标签的作为预测结果
# 对比预测值predict和真实标签y
if predict == y:
right += 1
else:
# 将识别错误的样例打印了出来
img_path = test_dataset.samples[i][0]
print(f"wrong case: predict = {predict} y = {y} img_path = {img_path}")
# 计算出测试效果
sample_num = len(test_dataset)
acc = right * 1.0 / sample_num
print("test accuracy = %d / %d = %.3lf" % (right, sample_num, acc))
加载测试集图片依次应用模型,并对比结果与真实值差异,保存差异值,打印并计数,算出模型整体准确率。
结果:
wrong case: predict = 5 y = 9 img_path = ./mnist_images/test\9\d49c3b8b4ff6153e.png
wrong case: predict = 7 y = 9 img_path = ./mnist_images/test\9\e2eabc1f4af0928c.png
wrong case: predict = 4 y = 9 img_path = ./mnist_images/test\9\e3763ad6230f4fe5.png
wrong case: predict = 5 y = 9 img_path = ./mnist_images/test\9\ec6c3a2fa9af3286.png
wrong case: predict = 7 y = 9 img_path = ./mnist_images/test\9\f4a08ca3756a8e9c.png
wrong case: predict = 3 y = 9 img_path = ./mnist_images/test\9\f52aa330abf33706.png
test accuracy = 9815 / 10000 = 0.982
进程已结束,退出代码为 0
模型准确率为 98.2%
本文内容参考了多篇相关文章,旨在进行学习与总结。如涉及版权问题,请作者及时联系删除,本人将不胜感激并深表歉意。
感谢您的观看!!!