一个最简单的神经网络详解
文章目录
引言:本文将以代码逻辑为线索,依次对代码中涉及到的相关知识进行讲解,由于本人也是才入门机器学习,搜索资料和理解能力还不够好,文中若有谬误,希望大家指出。另外本文主要参考同济子豪兄的B站视频及相关资料。附上链接:【子豪兄Pytorch】二十分钟搭建神经网络分类Fashion-MNIST数据集时尚物品_哔哩哔哩_bilibili
第一部分:导入需要使用的包
import torch
from torch import nn, optim # 导入神经网络与优化器对应的类
import torch.nn.functional as F
from torchvision import datasets, transforms # 导入数据集与数据预处理的方法
import torch.utils.data # 解决cannot find reference 'data' in '_init_.py_'
第一句:
torch
是一个包含大量机器学习算法的科学计算框架(就是一个很大的仓库,里面有你可能需要的各种方便计算的方法,基础的向量之间的距离计算、向量转置、索引、切片、数学运算,高级一点的数据集载入和变换方法等等,你就不用自己去造轮子了),里面最重要的一个概念——Tensor(张量),一种数据类型,常常和numpy中的array进行比较,现阶段我对torch.Tensor()和np.array()的理解就是可以互相转换,torch.Tensor可以在GPU上计算,而np.numpy()不行。
pytorch
是为了让torch
能在python环境下更好地被调用而开发出来的,这里import torch
本质上是使用了pytorch
。
第二句:
nn
是Neural Network的简称,看名字就知道这是一个有关神经网络的模块,里面有专门为神经网络设计的模块化接口,你只需要去直接调用就好了,里面最常用的五个子类:
nn.functional
:定义了创建神经网络所需要的常见函数nn.Module
:一个抽象概念,既可以表示神经网络中的某一层,也可以表示整个神经网络nn.Parameter
:了解的不多nn.Linear
:全连接层,这个在理论学习的时候你肯定就知道是啥了吧nn.Sequential
:了解的不多
这里只是简单介绍了一下,想更深入了解的可以先去看这几篇我参考的文章。
optim
是一个优化神经网络的库,是所有优化方法的父类,后面的Adam
梯度下降方法就是出自optim
第三句:
将torch
中的nn.functional
用F
替换,后面的代码中F
就代表了nn.functional
第四句:
从torchvision
中将datasets
和transforms
两个包引入
torchvision
介绍:这是一个独立于pytorch
,专门用来处理图像问题的包,后面对数据集的载入和变换就要用到,他有三个常见的包:
models
:提供了很多常用的训练好的网络模型,直接白嫖使用,让你马上跑起来。datasets
:提供了一些常见的图片数据集,如MNIST、FashionMNIST、COCO、CIFAR10/100等等transforms
:提供了一些常用的图像转换处理的操作,后面会介绍两种。
第五句:
这句话是解决后面这句代码报错:
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
# 报错提示:cannot find reference 'data' in '_init_.py_'
第二部分:定义main函数
def print_hi(name):
# 在下面的代码行中使用断点来调试脚本。
print(f'Hi, {name}') # 按 Ctrl+F8 切换断点。
# 按间距中的绿色按钮以运行脚本。
if __name__ == '__main__':
print_hi('PyCharm')
print(torch.__version__)
这里我就统一说了,python的启动和C/C++真不太一样,我已经习惯了C/C++严谨的风格,现在看python语法就比较难受。
在C/C++语法中,整个项目的启动是在main()函数中开始的,但在python中却不是这样,python使用缩进对齐来组织代码的执行,敲重点:所有没有缩进的代码(除了函数定义和类定义)都会在载入时自动执行,比如以下代码:
def print_hi(name):
print(f'Hi, {name}')
print(1)
if __name__ == '__main__':
print_hi('PyCharm')
print(torch.__version__)
# 运行结果为:
1
Hi, PyCharm
2.1.0
解释一下:
首先他顺次执行,先到达def print_hi(name)
这一句,发现是函数定义,所以跳过
到了print(1)
执行,输出1
到了if name == ‘main’:
,如果你是执行的这个文件,那name
就是main
,并顺利输出Hi, PyCharm,如果是调用的这个文件,那name
就是该文件的文件名,可能就不会执行下面的内容(取决于你文件名是不是叫main.py)
总而言之,就是从头到尾执行,遇到函数定义和类定义就跳过,你从上往下看就行了。
第三部分:导入数据库
# 数据预处理,标准化图像数据,使得灰度数据在-1到+1之间
# transforms是一个专门用来对数据进行变化操作的一个torch的子库
# Compose可以看成一种容器,容器内放入的是一个列表[], 这个列表中放的就是对数据进行何种变换
# 这里可以看到是执行了将img转变成Tensor类型并且进行了数据标准化操作
# transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.2861,), (0.3529,))])
transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
# 下载Fashion-MNIST训练集数据,并构建训练集数据载入器trainloader, 每次从训练集中栽入64张图片,每次栽入都打乱顺序
# datasets这个库中有许多常用的数据集, 对于这些已有的数据集进行栽入可以直接用datasets的子类方法
# 第一项指定要将数据集导入到哪个位置,第二项指定如果该位置不存在就进行下载, 第三项指定是train还是test, 第四项指定对数据的变换操作
trainset = datasets.FashionMNIST(root='dataset/', download=False, train=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
# 这是用来训练得到mean和std的
# trainset = datasets.FashionMNIST(root='dataset/', download=False, train=True, transform=transforms.ToTensor())
# trainloader = torch.utils.data.DataLoader(trainset, batch_size=256, shuffle=True)
# 这是MNIST数据集
# trainset = datasets.MNIST(root="dataset/", download=True, train=True, transform=transform)
# trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
# 下载Fashion-MNIST测试集数据, 并构建测试集数据载入器trainloader, 每次从测试集中载入64张图片,每次栽入都打乱顺序
testset = datasets.FashionMNIST(root='dataset/', download=False, train=False, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=True)
# 这是MNIST数据集
# testset = datasets.MNIST(root="dataset/", download=True, train=False, transform=transform)
# testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=True)
第一句:
已经知道transforms
是一个对图像进行变换处理操作的库了,这里Compose
你先可以浅显地理解成一个容器,后面跟一个列表,列表里就是你准备对数据集动的手脚,比如我这里就是先转化成Tensor
结构,然后使用归一化操作,使得图像数据都保证在[-1,1]。
第二句
trainset
就是将datasets
库中的FashionMNIST
导入到代码中的数据,其中参数root
是指定数据集存放的位置,参数download
是当指定位置没有数据时是否要去网上下载,参数train
是指这里载入的是训练集还是测试集,参数transform
是对原始数据进行何种操作(由于原始数据可能大小、尺寸、形状不统一,无法训练),这里我们就是用的第一句中transforms.ToTensor()
和 transforms.Normalize((0.5,), (0.5,))
这两个数据处理方法。
第三句
trainloader
是从trainset
中抽取的一部分数据,这里使用了torch
中对数据进行加载的一个常用方法,参数trainset
是自己载入的数据集数据,参数batch_size
是抽取的数量,我这里就是每次抽取64张图片到trainloader
中,参数shuffle
是是否打乱数据集来抽取
后面两句与二三句一样
第四部分:定义神经网络模型的类
class Classifier(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(28*28, 256)
self.fc2 = nn.Linear(256, 128)
self.fc3 = nn.Linear(128, 64)
self.fc4 = nn.Linear(64, 10)
# 构造Dropout方法, 在每次训练过程中都随机“掐死”百分之二十的神经元, 防止过拟合
self.dropout = nn.Dropout(p=0.2)
def forward(self, x):
# make sure input tensor is flattened
# 确保输入的tensor是展开的单列数据, 把每张图片的通道、长度、宽度三个维度压缩成一列
x = x.view(x.shape[0], -1)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = F.relu(self.fc3(x))
x = self.dropout(x)
# x = F.sigmoid(self.fc1(x))
# x = F.sigmoid(self.fc2(x))
# x = F.sigmoid(self.fc3(x))
# 在输出单元不需要使用Dropout方法
x = F.log_softmax(self.fc4(x), dim=1)
return x
定义构造函数部分:
这里引用了父类的构造函数,然后定义了fc1
、fc2
、fc3
、fc4
、dropout
这五个属性值,fcx
是定义了全连接层的神经元个数(说明一下,这里的Linear(in_features, out_features, bias=True)
看做是神经网络两层之间的连接部分,也就是线性转换部分(y=wx+b),其中的参数in_features
和out_features
分别是上一层输出神经元个数和下一层输入神经元个数,bias
表示是否使用偏置,也就是公式中的b
。参考:nn.Linear()的基本用法
ps:这里面的28*28是根据数据集的图像大小确定了,FashionMNIST数据集的图像是28*28像素的灰度图像,分为10大类,所以输入为28*28,输出是根据你分为几大类来确定的,这里是10个类别,所以最后一个全连接层的输出神经元个数是10。中间的神经元个数可以自己定,但一般是有经验方法来摸索出一个数量、层数比较合适的神经元结构。
nn.dropout
方法是用来随机嘎掉一部分神经元的输出(注意是在执行激活函数之后嘎掉,将输出置0),p=0.2
就是说每个神经元的输出都有20%的概率被嘎掉,主要功能是防止某些神经元“一家独大”,使得整个神经网络的训练效果大打折扣,也适用于防止网路overfitting。
前向传播方法部分:
第一句: x = x.view(x.shape[0], -1)
这里view()
相当于reshape
、resize
方法,重新调整Tensor的形状,原本的x是64张单通道28*28的图片,shape
值为[64, 1, 28, 28]
,现在这里指定x
矩阵只有两个维度,行为64,列自动计算。所以实现的效果是将每张图片的通道、长、宽都压缩成一行。x
就变成了[64, 784]
,符合神经网络输入层的矩阵相乘的格式([64, 784]
*[784, 256]
)。参考:view()的基本用法
后三句:x = F.relu(self.fc1(x))
这里先执行了self.fc1(x)
用于从输入单元映射到第一层隐层,然后执行F.relu
使用了functional
库中的relu
激活函数(当然你可以选择其他的,比如sigmoid),得到在第一层隐层的输出数据,后面同理。
第五句:x = self.dropout(x)
掐死20%的神经元输出
倒数第二句:x = F.log_softmax(self.fc4(x), dim=1)
这里先从最后一个隐层映射到输出层,输出层的Tensor结构为矩阵[64, 10]
。然后使用对数正则化,dim=1
表示每一列的值之和为1,参考:functional.softmax的使用
最后一句:return x
返回结果
第五部分:定义训练过程中需要用到的损失函数、优化方法
# 对上面定义的Classifier类进行实例化
model = Classifier()
# 定义损失函数为负对数损失函数
criterion = nn.NLLLoss()
# 优化方法为Adsam梯度下降方法, 学习率为0.003
optimizer = optim.Adam(model.parameters(), lr=0.01) # learn rate
# 对训练集的全部数据学习15遍
epochs = 15
# 将每次训练的训练误差和测试误差存储在这两个列表中,后面绘制误差变化折线图
train_losses, test_losses = [], []
第一句:实例化
第二句:定义损失函数
在理论学习中你应该已经知道损失函数是啥了吧,这里简单介绍一下就是你在前向传播得出结果后和原本的label进行比较,看差距有多少(计算差距的方法有许多,这里使用的是负对数),然后用这个差距(损失值)进行反向传播更新nn.Linear
中的参数w
和bias
。
第三句:定义优化方法
前面得到了损失值,在反向传播的时候如何利用损失值来更新参数,更新力度多大,这就是优化方法要做的事情,这里选用的是optim
库中的Adam
方法,学习率Learn rate
为0.003
第四句:迭代次数
就是对数据集中的数据学习多少遍,迭代次数越多,时间越长,超过适当时间会产生overfitting。
第五句:将每次迭代的损失值保存下来画图
第六部分:开始训练
print('开始训练')
for e in range(epochs): # 表示要迭代epochs次
running_loss = 0
#对训练集中的所有图片都过一遍
for images, labels in trainloader:
# 将优化器中的求导结果都设置为0, 否则会在每次反向传播之后叠加之前的
optimizer.zero_grad()
# 对64张图片进行推断, 计算损失函数, 方向传播优化权重, 将损失求和
log_ps = model(images) # 自动调用forward前向传播得到神经网络的输出
loss = criterion(log_ps, labels) # 调用损失函数计算损失值
loss.backward()
optimizer.step()
running_loss += loss.item()
首先迭代epochs次
然后取出图片信息images
和对应的标签labels
,这里的images
和labels
是包含了64个图片的信息,至于这里trainloader
本身只有64个数据,但这里貌似使用了迭代器,那images
不是一次性全拿到了吗,为啥要迭代呢,我自己想法是这里trainloader
其实也是在trainset
中迭代,当images
取出数据后,自己就在trainset
中更新。
然后是这句optimizer.zero_grad()
,这里是将内含的梯度值清除,如果不清除的话,后面的loss.backward()
就会把这次的梯度和之前的梯度加起来,明显这是不对的。
然后就是把图片数据喂给神经网络log_ps = model(images)
,这里底层的实现细节我稍微看了一下,大概就是会调用一个call
函数,然后call
函数调用call_impl
函数,然后call_impl
函数调用forward
函数进行前向传播。得到一轮对各个标签的预测值。
然后调用损失函数,将预测值和实际值传入损失函数中计算损失值。
然后对损失值使用反向传播来计算更新梯度
使用优化器根据设定的参数来优化全连接层中的参数
将损失值累计在running_loss
中。
第七部分:在测试集上验证
# 每次学完一遍数据集, 都进行一下测试
else:
test_loss = 0
accuracy = 0
# 测试的时候不需要开自动求导和反向传播
with torch.no_grad():
# 关闭Dropout
model.eval()
# 对测试集中的所有图片都过一遍
for images, labels in testloader:
# 对传入的测试集图片进行正向推断, 计算损失函数, accuracy为测试集一万张图片中模型预测正确率
log_ps = model(images)
test_loss += criterion(log_ps, labels)
ps = torch.exp(log_ps)
top_p, top_class = ps.topk(1, dim=1)
equals = top_class == labels.view(*top_class.shape)
# 等号右边为每一轮张测试图片中预测正确的占比
accuracy += torch.mean(equals.type(torch.FloatTensor))
# 恢复Dropout
model.train()
# 将训练误差和测试误差存在两个列表里, 后面绘制误差变化折线图
train_losses.append(running_loss/len(trainloader))
test_losses.append(test_loss/len(testloader))
print("训练集学习次数: {}/{}..".format(e+1, epochs),
"训练误差: {:.3f}..".format(running_loss/len(trainloader)),
"测试误差: {:.3f}..".format(test_loss/len(testloader)),
"模型分类准确率: {:.3f}".format(accuracy/len(testloader)))
这里else
要讲一下和C语言的区别,在python
中else不仅可以跟在if
后面,还可以跟在for
、while
后面,含义为:当for
或者while
正常执行退出(不在循环内break
)就执行一遍else
的内容,代码中指的是当对所有数据学习完一次epochs
时就测试一次。
定义了测试集上的损失值test_loss
和预测准确率accuracy
with
的用法:对于一些事先需要设置,事后需要做清理工作的场景的一种优化处理,比如我们要导入一个.txt
文件的数据,要file.open
和file.close
,但with
就能自动执行这两个操作,像下面这样就要方便许多:
with open("/tmp/foo.txt") as file:
data = file.read()
对于torch.no_grad()
,首先要介绍requires_grad
,这是Tensor
的一个参数,如果设置为True
那在反向传播的时候这个张量就会自动求导,而torch.no_grad()
就是将所有计算得到的Tensor
的requires_grad
设置为False
。至于为什么这么做,测试的时候本来就是验证当前训练情况的,测试本身是不算做训练的,所以不用更新网络啊。
model.eval
将神经网络中的Batch Normalization
和Dropout
这两层停止,对应的启动这两层的方法是model.train
,你可以把这两个模式看做是测试模式和训练模式(参考文章)。这里有个问题就是你停止了Dropout
,那神经网络的所有输出都有效,这导致你预测时的状态和你训练时的状态不一样(一个恰当的比喻:就像训练时你跑步有负重,比赛的时候没负重了,但往往不会导致好的结果)。所以会有一定的补救措施,参考这篇文章,下面二选一:
- 在训练中,
Dropout
后,对输出值进行一个放大,每个神经元乘以1/(1-p)
。 - 在测试中,对每个神经元输出乘以
p
。
ps = torch.exp(log_ps)
是将log_ps
转换成指数形式,原来损失函数就是负对数形式的,用一个指数转化就变成了普通形式,前提是log_ps
必须是Tensor
类型。
ps.topk(1, dim=1)
是取ps
这个Tensor
的前k个数,这里是1,dim
是指按照哪个维度来取,dim=0
表示取行的前k个数,dim=1
表示取列的前k个数。默认是dim=1
。然后返回两个变量,分别是包含前k个数的Tensor
类型变量,和这些数在原来数据中的索引。这里用top_p
和top_class
表示。
equals = top_class == labels.view(*top_class.shape)
这句话分前部分和后部分
*top_class.shape
这里注意是作为实参传入到.view
函数中,在python中星号*
在形参前面表示的是收集参数,星号*
在实参前面代表是将输入的迭代器拆成一个个元素。labels.view
将标签结构重新排列成与top_class
相同的格式,以此来一一对比,得到equals
中的64个True或者False。
equals.type(torch.FloatTensor)
将类型变成浮点类型,然后在torch.mean
中求出平均值进行累加,以此来作为准确率的判断
做完测试之后恢复训练集model.train()
后面两个是将之前记录的损失值保存在一个数组中,方便后面画图来直观感受。
第八部分:画出直观图
import matplotlib.pyplot as plt
plt.plot(train_losses, label='Training loss')
plt.plot(test_losses, label='Validation loss')
plt.legend()
plt.show()
再次叠甲,这篇文章主要用来记录我自己一个阶段的学习,也是希望后来随着自己学习的深入然后回来修改,加深自己学习路上的脚印,同时也真心希望能帮助同样入门的同学能对一个最基础的神经网络有一个初步的了解,也希望各位大佬能在评论区留下指正。