多层感知机理论部分
本文系统的讲解多层感知机的pytorch复现,以及详细的代码解释。
部分文字和代码来自《动手学深度学习》!!
获取最新神经网络信息可以加我微信公众号
目录
隐藏层
上两节我们使用的网络都是线性的,即只有输入层和输出层两层,但生活中线性模型有很大的限制。例如,如何对猫和狗的图像进行分类呢? 增加位置处像素的强度是否总是增加(或降低)图像描绘狗的似然? 对线性模型的依赖对应于一个隐含的假设, 即区分猫和狗的唯一要求是评估单个像素的强度。 在一个倒置图像后依然保留类别的世界里,这种方法注定会失败。
所以我们在网络中加入隐藏层来克服线性模型的限制,使其能更普遍的处理函数关系模型。
要做到这一点,最简单的方法是将许多全连接层堆叠在一起。 每一层都输出到上面的层,直到生成最后的输出。 我们可以把前层看作表示,把最后一层看作线性预测器。 这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP。像下面的图示:
这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。 输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算。 因此,这个多层感知机中的层数为2。 注意,这两个层都是全连接的。 每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。
多层感知机数学逻辑
多层感知机(Multilayer Perceptron, MLP)是一种基于多个全连接层的前馈神经网络,其公式如下:
假设输入为 x ∈ R d \boldsymbol{x} \in \mathbb{R}^d x∈Rd,第 i i i 个全连接层的权重和偏差参数为 W i ∈ R h i − 1 × h i \boldsymbol{W}i \in \mathbb{R}^{h{i-1}\times h_i} Wi∈Rhi−1×hi 和 b i ∈ R 1 × h i \boldsymbol{b}_i \in \mathbb{R}^{1\times h_i} bi∈R1×hi,激活函数为 ϕ i ( ⋅ ) \phi_i(\cdot) ϕi(⋅)。输出为 o ∈ R q \boldsymbol{o} \in \mathbb{R}^{q} o∈Rq,则有:
h 0 = x h i = ϕ i ( h i − 1 W i + b i ) , i = 1 , 2 , … , n − 1 o = h n − 1 W n + b n \begin{aligned} \boldsymbol{h}_0 &= \boldsymbol{x} \\ \boldsymbol{h}i &= \phi_i(\boldsymbol{h}_{i-1} \boldsymbol{W}_i + \boldsymbol{b}i), \quad i=1,2,\ldots,n-1\\ \boldsymbol{o} &= \boldsymbol{h}_{n-1} \boldsymbol{W}_n + \boldsymbol{b}_n \end{aligned} h0hio=x=ϕi(hi−1Wi+bi),i=1,2,…,n−1=hn−1Wn+bn
其中 n n n 表示全连接层数, h i \boldsymbol{h}_i hi 表示第 i i i 层的输出, h 0 \boldsymbol{h}_0 h0 为输入, W n ∈ R h n − 1 × q \boldsymbol{W}n \in \mathbb{R}^{h{n-1}\times q} Wn∈Rhn−1×q 和 b n ∈ R 1 × q \boldsymbol{b}_n \in \mathbb{R}^{1\times q} bn∈R1×q 是输出层的权重和偏差参数。 h i h_i hi 表示第 i i i 层的输出维度,也就是该层的神经元个数。最后一层的激活函数一般不加,也可以视为 ϕ n ( ⋅ ) = i d e n t i t y ( ⋅ ) \phi_n(\cdot) = \mathrm{identity}(\cdot) ϕn(⋅)=identity(⋅)。
激活函数
激活函数将非线性特性引入到神经网络中。在下图中,输入的 inputs 通过加权,求和后,还被作用了一个函数f,这个函数f就是激活函数。引入激活函数是为了增加神经网络模型的非线性。没有激活函数的每层都相当于矩阵相乘。就算你叠加了若干层之后,无非还是个矩阵相乘罢了。
为什么使用激活函数
如果不用激活函数,每一层输出都是上层输入的线性函数,无论神经网络有多少层,输出都是输入的线性组合,这种情况就是最原始的感知机(Perceptron)。如果使用的话,激活函数给神经元引入了非线性因素,使得神经网络可以任意逼近任何非线性函数,这样神经网络就可以应用到众多的非线性模型中。
1. ReLU函数
最受欢迎的激活函数是修正线性单元(Rectified linear unit,ReLU), 因为它实现简单,同时在各种预测任务中表现良好。 ReLU提供了一种非常简单的非线性变换。 给定元素x,ReLU函数被定义为该元素与的0最大值:
R
e
L
U
(
x
)
=
m
a
x
(
x
,
0
)
ReLU(x) = max(x, 0)
ReLU(x)=max(x,0)
通俗地说,ReLU函数通过将相应的活性值设为0,仅保留正元素并丢弃所有负元素。 为了直观感受一下,我们可以画出函数的曲线图。 正如从图中所看到,激活函数是分段线性的。
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))
、
当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。 注意,当输入值精确等于0时,ReLU函数不可导。 在此时,我们默认使用左侧的导数,即当输入为0时导数为0。 我们可以忽略这种情况,因为输入可能永远都不会是0。
观察其求导图像
y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
观察图像可知,它求导表现得特别好:要么让参数消失,要么让参数通过。 这使得优化表现得更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题
2. sigmoid函数
sigmoid函数是一种常用的激活函数,是早期深度学习和神经网络研究比较常用的激活函数,但在现在使用较少,其常用于神经网络的输出层,将输入的值映射到[0, 1]区间内,公式如下:
s
i
g
m
o
i
d
(
x
)
=
1
1
+
e
x
p
(
−
x
)
sigmoid(x)=\frac{1}{1+exp(-x)}
sigmoid(x)=1+exp(−x)1
图像为:
其中,x为输入值。sigmoid函数的输出值范围在(0, 1)之间,当x为0时,sigmoid函数的值为0.5。sigmoid函数的导数可以用其自身表示:
σ ( x ) = 1 1 + e − x d d x σ ( x ) = e − x ( 1 + e − x ) 2 & = 1 1 + e − x ⋅ e − x 1 + e − x = σ ( x ) ( 1 − σ ( x ) ) \begin{aligned}\sigma(x) & =\frac{1}{1+e^{-x}} \ \frac{d}{dx} \sigma(x) &= \frac{e^{-x}}{(1+e^{-x})^2}\&=\frac{1}{1+e^{-x}}\cdot \frac{e^{-x}}{1+e^{-x}} \ &=\sigma(x)(1-\sigma(x))\end{aligned} σ(x)=1+e−x1 dxdσ(x)=(1+e−x)2e−x&=1+e−x1⋅1+e−xe−x =σ(x)(1−σ(x))
sigmoid函数具有连续可导、单调递增、输出值在(0, 1)之间等特点,但其缺点也比较明显,如当输入值过大或过小时,其导数趋于0,容易出现梯度消失的问题。
感兴趣的同学可以参照上面的代码自行绘制sigmoid函数导数的图像
3. tanh函数
tanh函数是双曲正切函数(hyperbolic tangent function)的缩写,数学表达式为: tanh ( x ) = 1 − e − 2 x 1 + e − 2 x \text{tanh}(x) = \frac{1 - e^{-2x}}{1 + e^{-2x}} tanh(x)=1+e−2x1−e−2x
图像为:
与sigmoid函数类似,tanh函数也是一种常用的激活函数。它的取值范围为
(
−
1
,
1
)
(-1, 1)
(−1,1),且在原点处取值为0。相比于sigmoid函数,在输入接近0的时候,tanh函数的函数值变化更为明显,可以使得模型更快地学习到特征。同时,tanh函数在原点两侧的函数值的变化也更加平缓,避免了在输入较大或较小的时候出现梯度消失的问题。
其导数为:
d
d
x
tanh
(
x
)
=
1
−
tanh
2
(
x
)
\frac{d}{dx}\operatorname{tanh}(x) = 1 - \operatorname{tanh}^2(x)
dxdtanh(x)=1−tanh2(x)
多层感知机的从零开始实现
数据集
我们同样使用fashion-MNIST数据集,为了与上一节softmax回归做对比,理解线性和非线性之间的差别。
import torch
from torch import nn
from d2l import torch as d2l
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
注:load_data_fashion_mnist函数在上一节实现过,这里我们直接调用李沐大佬做的d2l包中的函数,源码在上一节。
初始化参数模型
回想一下,Fashion-MNIST中的每个图像由784个灰度像素值组成。 所有图像共分为10个类别。 忽略像素之间的空间结构, 我们可以将每个图像视为具有784个输入特征 和10个类的简单分类数据集。 首先,我们将实现一个具有单隐藏层的多层感知机, 它包含256个隐藏单元。 注意,我们可以将这两个变量都视为超参数。 通常,我们选择2的若干次幂作为层的宽度。 因为内存在硬件中的分配和寻址方式,这么做往往可以在计算上更高效。
为了简化代码,我们实现三层的感知机,故参数为输入层到隐藏层的W1,b1和隐藏层到输出层的W2,b2
num_inputs, num_outputs, num_hiddens = 784, 10, 256
W1 = nn.Parameter(torch.randn(
num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(
num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))
params = [W1, b1, W2, b2]
Parameter 是一个 Tensor 的子类,在定义神经网络时常用于将需要优化的参数包裹在其中。与普通的 Tensor 不同, Parameter 会被自动添加到模型的参数列表中,以便在反向传播过程中自动求导更新参数。当我们在使用优化算法训练模型时,我们只需要调用模型的 parameters() 方法即可获得所有需要优化的参数。
torch.randn(num_inputs, num_hiddens, requires_grad=True) 生成了一个大小为 (num_inputs, num_hiddens) 的张量,表示输入和隐藏层之间的权重。由于该张量是需要被优化的,因此 requires_grad=True 表示该张量的导数需要被计算和存储,以便在反向传播中计算梯度。
生成的随机数乘以 0.01 的目的是将其缩小,从而避免初始参数值过大,导致神经网络训练过程中出现梯度消失或爆炸的问题。
激活函数
def relu(X):
a = torch.zeros_like(X)
return torch.max(X, a)
模型定义
def net(X):
X = X.reshape((-1, num_inputs))
H = relu(X@W1 + b1) # 这里“@”代表矩阵乘法
return (H@W2 + b2)
损失函数
我们采用交叉熵作为损失函数
loss = nn.CrossEntropyLoss(reduction='none')
训练
batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
其中train_ch3的实现部分在上一节
多层感知机的pytorch实现(pytorch实现神经网络的流程)
1. 数据集
import torch
from torch import nn
from d2l import torch as d2l
import torchvision
from torch.utils import data
from torchvision import transforms
def get_dataloader_workers(): #@save
"""使用4个进程来读取数据"""
return 4
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
trans = [transforms.ToTensor()]
if resize:
trans.insert(0, transforms.Resize(resize))
trans = transforms.Compose(trans)
mnist_train = torchvision.datasets.FashionMNIST(
root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
root="../data", train=False, transform=trans, download=True)
return (data.DataLoader(mnist_train, batch_size, shuffle=True,
num_workers=get_dataloader_workers()),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=get_dataloader_workers()))
模型定义及参数初始化
net = nn.Sequential(nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
nn.Linear(256, 10))
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights);
损失函数,优化器和超参数的定义
loss = torch.nn.CrossEntropyLoss()#损失函数
lr = 0.01#学习率
optim = torch.optim.SGD(net.parameters(),lr=lr)#优化器
epoch = 10
batch_size=256
tensorboard的初始化
tensorboard用于存储和可视化损失函数和精确度等数据
from torch.utils.tensorboard import SummaryWriter
log_dir = ''
writer = SummaryWriter(log_dir)
训练
train_iter, test_iter = load_data_fashion_mnist(batch_size)
train_step = 0 #每轮训练的次数
net.train()#模型在训练状态
for i in range(epoch):
print("第{}轮训练".format(i+1))
train_step = 0
total_loss = 0.0#总损失
total_correct = 0#总正确数
total_examples = 0#总训练数
for X,y in train_iter:
# 前向传播
output=net(X)
l=loss(output,y)
# 反向传播和优化
optim.zero_grad()
l.backward()
optim.step()
#记录/输出损失和精确率
total_loss += l.item()
total_correct += (output.argmax(dim=1) == y).sum().item()
total_examples += y.size(0)
# 记录训练损失和准确率到 tensorboard
writer.add_scalar('train/loss', l.item(), epoch * len(train_iter) + i)
writer.add_scalar('train/accuracy', total_correct/total_examples, epoch * len(train_iter) + i)
train_step+=1
if(train_step%100==0):#每训练一百组输出一次损失
print("第{}轮的第{}次训练的loss:{}".format((i+1),train_step,l.item()))
# 在测试集上面的效果
net.eval() #在验证状态
total_test_correct = 0
total_test_examples = 0
with torch.no_grad(): # 验证的部分,不是训练所以不要带入梯度
for X,y in test_iter:
outputs = net(X)
test_result_loss=loss(outputs,y)
total_test_correct += (outputs.argmax(1)==y).sum()
total_test_examples += y.size(0)
print("第{}轮训练在测试集上的准确率为{}".format((i+1),(total_test_correct/total_test_examples)))
这段代码是用于训练和测试深度学习模型的代码。首先使用 load_data_fashion_mnist(batch_size) 函数载入了Fashion-MNIST数据集并将训练数据和测试数据分别封装成 train_iter 和 test_iter 迭代器。
然后,将模型设置为训练状态:net.train()。接下来,开始训练模型。epoch 表示要迭代的轮数,外层循环控制训练轮数。内层循环遍历 train_iter 迭代器中的所有数据,并通过前向传播和反向传播更新模型参数。
在训练过程中,通过 total_loss、total_correct 和 total_examples 分别记录总的损失值、总的正确预测数量和总的样本数量,用于计算训练过程中的损失值和精确度。同时,将训练损失和准确率写入 tensorboard 中,以便后续可视化分析。
每训练一百次,将当前训练的损失值输出显示。
在训练完一轮后,使用 net.eval() 将模型转换为测试状态。在 with torch.no_grad() 的上下文管理器中,遍历 test_iter 迭代器中的所有测试数据,并通过模型进行预测,计算总的正确预测数量和总的样本数量,以计算测试准确率。最后,输出该轮训练在测试集上的准确率。
结果:
预测
def predict(net, test_iter, n=6): #@save
for X, y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
predict(net, test_iter)
结果如下:
模型的保存与获取
torch.save(net,'这里填保存地址')
modlue = torch.load('这里填模型地址')