文章目录
- 1 torchvision库与加载内置图片数据集
- 2 多层感知器
- 3 激活函数
- 3.1 ReLU激活函数
- 3.2 Sigmoid激活函数
- 3.3 Tanh激活函数
- 3.4 LeakyReLU激活函数
学习笔记
1 torchvision库与加载内置图片数据集
torchvision
库是PyTorch中用来处理图像和视频的一个辅助库,提供了一些常用的数据集、模型、转换函数等。
torchvision库提供的内置数据集可用于测试、学习和创建基准模型,为了统一数据加载和处理代码,PyTorch提供了两个类用以处理数据加载,它们分别是torch.utils.data.Dataset
类和torch.utils.data.DataLoader
类,通过这两个类可使数据集加载和预处理代码与模型训练代码脱钩,从而获得更好的代码模块化和代码可读性。
torchvision加载的内置图片数据集均继承自torch.utils.data.Dataset类,因此可直接使用加载的内置数据集创建DataLoader。
PyTorch的内置图片数据集均在torchvision.datasets模块下,包含Caltech、CelebA、CIFAR、Cityscapes、COCO、Fashion-MNIST、ImageNet、MNIST等很多著名的数据集。其中MNIST数据集是手写数字数据集,这是一个很适合入门者学习使用的小型计算机视觉数据集,它包含0~9的手写数字图片和每一张图片对应的标签。
下面以此数据集为例学习如何加载使用内置图片数据集,加载内置图片数据集的代码如下:
import torchvision
from torchvision.transforms import ToTensor
train_ds = torchvision.datasets.MNIST('data/', train=True, transform=ToTensor(), download=True)
test_ds = torchvision.datasets.MNIST('data/', train=False, transform=ToTensor(), download=True)
上述代码中,首先导入了torchvision库,并从torchvision.transforms
模块下导入ToTensor
类。
torchvision.transforms模块包含了转换函数,使用它可以很方便地对加载的图像做各种变换。
在这里我们用到了ToTensor
类,该类的主要作用有以下3点。
-
(1) 将输入转换为张量。
-
(2) 将读取图片的格式规范为(channel,height,width),这与我们以前经常遇到的图片格式可能有些区别,PyTorch中的图片格式一般是通道数(channel)在前,然后是高度(height)和宽度(width)。
-
(3) 将图片像素的取值范围归一化,规范为0~1。
上述加载代码中,通过torchvision.datasets.MNIST
方法加载MNIST数据集,
方法中的第一个参数data/表示下载数据集存放的位置,这里放在了当前程序目录下的data文件夹中;
参数train
表示是否是训练数据,若为True,则加载训练数据集,若为False,则加载测试数据集;
使用参数transform
表示对加载数据的预处理,参数值为ToTensor();
最后一个参数download=True
表示将下载此数据集,一旦下载完成后,下一次执行此代码时,将优先从本地文件夹直接加载。
现在我们得到了两个数据集,分别是训练数据集和测试数据集,PyTorch还提供了torch.utils.data.DataLoader
类用以对数据集做进一步的处理,DataLoader接收数据集,并执行复杂的操作,如小批次处理、多线程、随机打乱等,以便从数据集中获取数据。它接收来自用户的Dataset实例,并使用采样器策略将数据采样为小批次。
DataLoader主要有以下4个目的。
- (1)使用shuffle参数对数据集做乱序的操作。
一般情况下,需要对训练数据集进行乱序的操作。因为原始的数据在样本均衡的情况下可能是按照某种顺序进行排列的,如数据集的前半部分为某一类别的数据,后半部分为另一类别的数据。但经过打乱顺序之后,数据的排列就会拥有一定的随机性,在顺序读取的情况下,读取一次得到的样本为任何一种类型数据的可能性相同。这样可避免出现模型反复依次序学习数据的特征或者学习到的只是数据的次序特征的情况。
- (2)将数据采样为小批次,可用batch_size参数指定批次大小。
若同时对输入和标签迭代送入模型进行训练,这样单个样本训练有一个很大的缺点,就是损失和梯度会受到单个样本的影响,如果样本分布不均匀,或者有错误标注样本,则会引起梯度的巨大震荡,从而导致模型训练效果很差。为了解决此问题,可考虑使用批量数据训练(也叫作批量梯度下降算法),通过遍历全部数据集算一次损失函数,然后计算损失对各个参数的梯度,并更新参数。这种训练方式每更新一次,参数都要把数据集里的所有样本都看一遍,不仅计算开销大,而且计算速度慢。
为了克服上述两种方法的缺点,一般采用的是一种折中手段进行损失函数计算:即把数据分为若干个小的批次(batch),按批次来更新参数,这样,一个批次中的一组数据共同决定了本次梯度的方向,大大降低了参数更新时的梯度方差,下降起来更加稳定,减少了随机性。与单样本训练相比,小批次训练可利用矩阵操作进行有效的梯度计算,计算量也不是很大,对计算机内存的要求也不高。
-
(3)可以充分利用多个子进程加速数据预处理, num_workers参数可以指定子进程的数量。
-
(4)可通过collate_fn参数传递批次数据的处理函数,实现在DataLoader中对批次数据做转换处理。
train_dl = torch.utils.data.DataLoader(train_ds, batch_size=64,shuffle=True)
test_dl = torch.utils.data.DataLoader(test_ds, batch_size=46)
上述代码中分别创建了训练数据和测试数据的DataLoader,并设置它们的批次大小为64,对训练数据设置了shuffle为True;
对测试数据,由于仅仅作为测试,没必要做乱序。
DataLoader是可迭代对象,我们观察它返回的数据集的形状,以方便对DataLoader和MNIST数据集有一个直观的印象,代码如下:
imgs,labels = next(iter(train_dl))# 创建生成器,并用next方法返回一个批次数据
print(imgs.shape) # 输出torch.Size([64, 1, 28, 28])
print(labels.shape) # 输出torch.Size([64])
上述代码中使用iter方法将DataLoader对象创建为生成器,并使用next方法返回了一个批次的图像(imgs)和对应的一个批次的标签(labels),imgs.shape为torch.Size([64,1, 28, 28]),如何理解这个shape呢?
很显然,这里的64是批次,我们可以认为这代表64张形状为(1, 28, 28)的图片,其中1为通道数,28和28分别表示高和宽;既然这里有64张图片,对应的也应该有64个标签,也就是labels.shape所显示的torch.Size([64])。
下面通过绘图来看一下MNIST数据集中的这些图片是什么样子的。使用Matplotlib库绘图,绘制imgs中的前10张图片,代码如下:
2 多层感知器
单个的神经元所完成的任务就是对所有输入的特征乘以权重(w)、加上偏置(b)。
例如,输入有3个特征,分别为x1、x2、x3,那么线性回归模型所做的就是对这些输入特征乘以权重(w)、加上偏置(b),使用激活函数(Activation)激活后,得到输出(Prediction),公式如下:
P
r
e
d
i
c
t
i
o
n
=
A
c
t
i
v
a
t
i
o
n
(
w
1
×
x
1
+
w
2
×
x
2
+
w
3
×
x
3
+
b
)
Prediction=Activation(w1×x1+w2×x2+w3×x3+b)
Prediction=Activation(w1×x1+w2×x2+w3×x3+b)
当然,在回归问题上并不需要激活函数,或Activation相当于将计算结果直接传递输出。
上面这个公式可看作一个多变量的线性回归模型,一旦通过训练得到了模型的参数值(也就是w1、w2、w3和b的值),就可以使用这个模型做多变量的预测,只需要将参数代入上面的公式即可。
总结单个神经元的一般结构如图所示:
这里神经元是单层的,也就是说,只有一层计算就输出了结果,没有什么深度。
那什么是深度学习
呢?所谓深度学习就是使用更深层的网络来学习,而不是只有一层。
这种只有一层的模型,我们叫作浅层学习,经典机器学习中经常使用这种网络。
浅层学习有一个缺陷,也就是单层神经元的一个缺陷,它无法拟合异或运算,异或运算看上去非常简单,相同为0相异为1。
但是看似简单,使用单个的神经元的缺陷却没有办法解决。单个神经元要求数据必须是线性可分的,异或问题无法找到一条直线分割这两个类,这个问题使得神经网络的发展停滞了很多年。当然,对于无法线性可分的问题,在经典机器学习中可以通过将特征映射到高维空间来实现线性可分。
为了解决此问题,人们提出了使用多层神经元创建模型。多层神经元互相连接,使得模型的深度大大增加,这也是深度学习这个名字的由来。多层神经元互相连接一般称为多层感知器或者神经网络。那为什么叫它神经网络呢?人们解决拟合异或问题,其实受到了生物神经元的启发。生物大脑中的神经元是互相连接的,构成的神经网络彼此连接变得非常的深,当一个信号传递到某一个神经元,如果信号达到了一定的强度,此神经元会被激活,将信号往下传递;如果传递过来的信号没有激活此神经元,这个信号就不再往下传递。这便是大脑中神经元大致的工作原理,它有以下两个特点:
(1)互相连接,网络非常深。
(2)对信号有一个判断或激活。
深度学习或神经网络的工作方式与生物大脑神经元的这种传递的特点类似,为了继续使用神经网络拟合异或问题或者解决各种不具备线性可分的问题,人们想出了一个方法,就是在神经网络的输入和输出之间插入更多的神经元。如下图所示为多层感知器的图示:
输入层(input)和输出层(output)之间插入了一个隐藏层(hidden),这样使得网络层数变深,我们称它为多层感知器。在多层感知器这个网络结构中,每一层之间的所有单元均互相连接,每一个单元均与前一层所有单元连接,所以我们也称这样的网络为全连接网络。多层感知器的显著特点是包含多层神经元,上图中只有一个隐藏层,实际上可以添加更多的层,层越多,模型的拟合能力会越大。
3 激活函数
首先,为什么要激活?激活也是模拟了上面提到的生物大脑中神经元的工作原理,大脑中神经元会对接收到的信号进行考察,如果此信号满足一定条件,神经元才会将信号往下输出,反之就不往下传递了。激活函数的作用也类似,激活会为网络带来非线性,使得网络可以拟合非线性问题等更多复杂问题,大大增强网络的拟合能力。假如没有激活函数,无论输入与输出之间插入多少层,整个网络仍然是一个线性网络,它还是只能拟合线性问题。
总结起来,激活函数为网络带来了非线性,使得网络可以拟合各种各样复杂的问题。神经网络中常见的激活函数有ReLU、Sigmoid、Tanh、LeakyReLU等,激活函数也可以自定义,只要一个函数是线性可导的,就可以拿来作为激活函数。下面介绍几个常见的。
3.1 ReLU激活函数
ReLU也叫修正线性单元,是目前应用最多的一个激活函数,在当前大部分模型中,都使用ReLU函数作为激活函数,其公式如下:
f
(
x
)
=
m
a
x
(
x
,
0
)
f(x)=max(x,0)
f(x)=max(x,0)
ReLU激活函数图像如下图所示:
ReLU激活函数的特点是,如果一个输入是大于0的,将其往后传递,反之则输出0,ReLU激活函数在实际应用中几乎是当前最受欢迎的激活函数,它是大多数神经网络的默认选择。在PyTorch中有内置的torch.relu()方法用来实现ReLU激活,代码如下:
代码中可以看到如果输入中有小于0的元素,输出会被置为0。
3.2 Sigmoid激活函数
Sigmoid激活函数是神经网络早期最常用的激活函数,其公式如下:
S
i
g
m
o
i
d
(
x
)
=
1
/
(
1
+
e
−
x
)
Sigmoid(x)=1/(1+e-x)
Sigmoid(x)=1/(1+e−x)
Sigmoid激活函数图像如下图所示:
从上图中可以看到,Sigmoid函数的输出为0~1,其在输入0附近的曲线斜率比较大,在远离0的部分的曲线斜率接近于0。
Sigmoid函数也常作为逻辑回归问题的输出函数,这时Sigmoid的输出可以认为是一个是或否的概率值,常用来解决二分类问题。如果对经典机器学习中的逻辑回归有所了解的话,就会发现,逻辑回归模型的输出层就使用了Sigmoid函数。
Sigmoid函数在深度学习的早期是最常用的中间层激活函数,但是后来逐渐被ReLU函数所取代,这是因为Sigmoid函数在远离0这个中心点的区域几乎接近于水平线,会导致梯度的大大衰减,甚至发生梯度消失,使得模型不容易训练。
在PyTorch中有内置的torch.sigmoid()方法用来实现Sigmoid激活,代码如下:
input = torch.randn(2)
output = torch.sigmoid(input) #使用Sigmoid函数激活,输出为(0,1)
print(output) # 类似 tensor([0.4569, 0.5355])
3.3 Tanh激活函数
Tanh激活函数,又叫作双曲正切激活函数,其公式如下:
T
a
n
h
(
x
)
=
(
e
x
+
e
−
x
)
/
(
e
x
−
e
−
x
)
Tanh(x)=(ex+e-x)/(ex-e-x)
Tanh(x)=(ex+e−x)/(ex−e−x)
Tanh激活函数的图像如下图所示:
在形状上,Tanh激活函数类似Sigmoid激活函数,但是它的中心位置是0,其输出为(−1, 1)。
Tanh激活函数的一个常见的使用场景是对生成模型的输出做激活,从而使得输出规范为(−1, 1)。
PyTorch的内置函数torch.tanh()用来实现双曲正切激活函数。
input = torch.randn(2)
output = torch.tanh(input) # 对输入使用Tanh函数激活,输出为(-1, 1)
print(output) # 输出类似tensor([ 0.5529, -0.9574])
3.4 LeakyReLU激活函数
LeakyReLU
激活函数(带泄漏单元的ReLU)的数学表达式如下:
y
=
m
a
x
(
0
,
x
)
+
l
e
a
k
×
m
i
n
(
0
,
x
)
y=max(0,x)+leak×min(0,x)
y=max(0,x)+leak×min(0,x)
LeakyReLU
激活函数的图像如下图所示:
与ReLU
激活函数将所有的负值都设为零不同,LeakyReLU
激活函数给所有负值赋予一个小的非零斜率leak,这样保留了一些负轴的值,使得负轴的信息不会全部丢失。
实际中,LeakyReLU激活函数中的leak取值一般比较小,使用LeakyReLU
函数激活后,在反向传播过程中,LeakyReLU
激活函数输入小于零的部分,也可以计算得到梯度,这有利于一些情况下的模型训练,例如生成器模型。
PyTorch中有内置的LeakyReLU
实现,代码如下:
#初始化 LeakyReLU,参数表示负轴部分的斜率
m = nn.LeakyReLU(0.1)
input = torch.randn(2)
output = m(input)#使用LeakyReLU函数对输入激活
print(m)
print(input)
print(output)
'''
LeakyReLU(negative_slope=0.1)
tensor([-0.0633, 0.7387])
tensor([-0.0063, 0.7387])
'''
Leaky ReLU函数(ReLU的改进):
1、与ReLU函数相比,把x的非常小的线性分量给予负输入(0.01x)来调整负值的零梯度问题;有助于扩大 ReLU 函数的范围,通常𝜆λ的值为 0.01 左右;函数范围是负无穷到正无穷。
2、LeakyRelu激活函数通过在负半轴添加一个小的正斜率(使得负轴的信息不会全部丢失)来解决ReLU激活函数的“死区”问题,该斜率参数𝜆λ是手动设置的超参数,一般设置为0.01。通过这种方式,LeakyRelu激活函数可以确保模型训练过程中神经元的权重在输入小于0的情况下依然会得到更新。
3、不会出现 Dead ReLu 问题,但是关于输入函数f(x) 的部分容易出现梯度爆炸的情况是一样的,所以必要时也可以搭配 sigmoid 或 tanh 使用。
Leaky ReLU不足:
1、经典(以及广泛使用的)ReLU 激活函数的变体,带泄露修正线性单元(Leaky ReLU)的输出对负值输入有很小的坡度。由于导数总是不为零,这能减少静默神经元的出现,允许基于梯度的学习(虽然会很慢)。
2、从理论上讲,Leaky ReLU 具有 ReLU 的所有优点,而且 Dead ReLU 不会有任何问题,但在实际操作中,尚未完全证明 Leaky ReLU 总是比 ReLU 更好。
还有很多比较著名的激活函数,如ELU、ReLU6、PReLU、SELU等。
关于激活函数可参考链接自行了解:常用的激活函数合集
pytorch学习推荐日月光华老师的pytorch免费课程:pytorch深度学习与简明教程