多层感知机
一.隐藏层和激活函数
1.为什么需要隐藏层?
前面几篇博客我们通过基础知识,学习了如何处理数据,如何将输出转换为有效的概率分布, 并应用适当的损失函数,根据模型参数最小化损失。
但是记不记得当时我们算出来的数据都是线性的,我们把一张图片28*28=784的每一个像素视为一个特征进行标签预测,进行权重和偏置运算的时候都是线性的。
如何对猫和狗的图像进行分类呢? 增加位置(13,17)处像素的强度是否总是增加(或降低)图像描绘狗的似然? 对线性模型的依赖对应于一个隐含的假设, 即区分猫和狗的唯一要求是评估单个像素的强度。 在一个倒置图像后依然保留类别的世界里,这种方法注定会失败。
如果有非线性的问题出现的时候我们该怎么做呢?
任何像素的重要性都以复杂的方式取决于该像素的上下文(周围像素的值)。 我们的数据可能会有一种表示,这种表示会考虑到我们在特征之间的相关交互作用。
我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制, 使其能处理更普遍的函数关系类型。
这时候我们就引入了多层感知机模型
多层感知机(MLP)通常包括输入层、一个或多个隐藏层以及输出层。输入层接收外部数据,隐藏层对数据进行处理和变换,输出层则产生最终的结果
看下面这个图其实只有两层:隐藏层和输出层
这两个层都是全连接的。 每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。
下面这个是具体的矩阵范围
我们发现经过一次次输出还是等价的
所以我们隐藏层的特殊就体现出来了,我们把非线性的激活函数加到隐藏层里面
就像这样,括号前面那个就是激活函数,可以多加几个隐藏层
当然但隐藏层不意味着能解决所有问题,它只是能学习任何函数
2.激活函数
首先我们知道我们可以有多个隐藏层,那么我们每一个隐藏层的激活函数都一样还是不一样呀?
使用相同激活函数的情况:
- 简化设计:使用相同的激活函数可以简化模型的设计过程,减少需要调整的超参数数量。
- 一致性:在某些情况下,保持所有隐藏层使用相同的激活函数可能有助于保持模型输出的一致性或可解释性。
使用不同激活函数的情况:
- 优化性能:不同的激活函数具有不同的数学特性和优点,针对不同的任务和数据集,使用不同的激活函数组合可能有助于提升模型的性能。
- 解决梯度消失/爆炸问题:例如,在深度网络中,使用ReLU激活函数(或其变体,如Leaky ReLU、PReLU)可以帮助缓解梯度消失问题,而在靠近输出的层使用Sigmoid或Softmax激活函数则更适合于分类任务的输出。
- 特征表示:在某些情况下,不同的隐藏层可能负责学习不同类型的特征表示,使用不同的激活函数可以更好地适应这种需求。
常见的激活函数组合:
- ReLU及其变体:在大多数现代神经网络中,ReLU及其变体(如Leaky ReLU、PReLU、ELU等)因其计算简单、收敛速度快且能有效缓解梯度消失问题而广受欢迎。它们常被用作隐藏层的激活函数。
- Sigmoid/Tanh:尽管在深度网络中不如ReLU系列流行,但Sigmoid和Tanh激活函数在某些特定任务(如二分类问题的输出层)中仍然有其用武之地。
- Softmax:在多分类问题的输出层中,Softmax激活函数是一个常见的选择,因为它可以将输出转换为概率分布。
了解完这个之后我们首先要知道激活函数输出的东西叫做:活性值
2.0激活函数定义
在神经元中,输入的 inputs 通过加权,求和后,还被作用了一个函数,这个函数就是激活函数。引入激活函数是为了增加神经网络模型的非线性。如果不用激活函数,每一层输出都是上层输入的线性函数,无论神经网络有多少层,输出都是输入的线性组合,这种情况就是最原始的感知机(Perceptron)。激活函数给神经元引入了非线性因素,使得神经网络可以任意逼近任何非线性函数,这样神经网络就可以应用到众多的非线性模型中。
接下来我们学三个激活函数:
我都按函数公式+原函数图像+该函数导数图像+代码的形式来介绍了
2.1ReLU函数
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))
y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
torch.ones_like(x)设置一个和y形状相同的张量的权重梯度进行方向传播,如果设为zeros_like的话,那么导数就都为0了
这个我当时不太理解,还是多搜了搜终于懂了,写在下面了。
在PyTorch中,当你对一个非标量(即包含多个元素的张量)调用
.backward()
方法时,确实需要指定一个与这个张量形状相同的权重张量(通过gradient
参数,也称为grad_tensors
)上面我说了权重梯度:
x.grad
:
对于x
中的每个元素,其梯度将是y
中对应元素的梯度(如果有的话)乘以weights
中对应元素的值(权重梯度)。但是,由于y
的前三个元素是0,它们的梯度也是0,无论weights
中的值是多少。就比如说你relu算出来是1,然后你指定weights对于的值为2.0,那么你x.grad求出来的就是2.0
retain_graph=True,这个参数指定了是否保留计算图以供后续使用
通过导数图像我们发现输入0时不可导,因为现实工程数学不存在输入为0,所以为o时我们导数就视为0
优点:
ReLu的收敛速度比 sigmoid 和 tanh 快;
函数在x>0区域上,梯度不会饱和,解决了梯度消失问题;
计算复杂度低,不需要进行指数运算,只要一个阈值就可以得到激活值;
适合用于后向传播。
缺点:
ReLU的输出不是zero-centered(0均值);
Dead ReLU Problem(神经元坏死现象):在x<0时,梯度为0。这个神经元及之后的神经元梯度永远为0,不再对任何数据有所响应,导致相应参数永远不会被更新。。
产生这种现象的两个原因:1、参数初始化问题;2、learning rate太高导致在训练过程中参数更新太大。
ReLU不会对数据做幅度压缩,所以数据的幅度会随着模型层数的增加不断扩张。
它还有很多变体:
加了线性使即使是负数也可以通过
2.2sigmoid函数
通常叫做挤压函数
你看这个函数它会把范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值
y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
# 清除以前的梯度
x.grad.data.zero_()
y.backward(torch.ones_like(x),retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))
sigmoid函数的导数图像如下所示。 注意,当输入为0时,sigmoid函数的导数达到最大值0.25; 而输入在任一方向上越远离0点时,导数越接近0
优点:
连续函数,便于求导的平滑函数;
能压缩数据,保证数据幅度不会有问题;
适合用于前向传播
缺点:
1.容易出现**梯度消失(gradient vanishing)**的现象:当激活函数接近饱和区时,变化太缓慢,导数接近0,根据后向传递的数学依据是微积分求导的链式法则,当前导数需要之前各层导数的乘积,几个比较小的数相乘,导数结果很接近0,从而无法完成深层网络的训练。
2.在反向传播时,当梯度接近于0,权重基本不会更新,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练。
3.Sigmoid的输出不是0均值(zero-centered)的:这会导致后层的神经元的输入是非0均值的信号,这会对梯度产生影响。以 f=sigmoid(wx+b)为例, 假设输入均为正数(或负数),那么对w的导数总是正数(或负数),这样在反向传播过程中要么都往正方向更新,要么都往负方向更新,导致有一种捆绑效果,使得收敛缓慢。(
在深度学习中,零均值化(zero-mean)处理常用于预处理喂给网络模型的训练图片。具体做法是让所有训练图片中每个位置的像素均值为0,使得像素值范围变为[-128,127],以0为中心。这样做的优点是为了在反向传播中加快网络中每一层权重参数的收敛。)
4.计算复杂度高,因为sigmoid函数是指数形式。幂运算相对耗时
2.3tanh函数
tanh把输入的数据转换为了-1到1之间
并且图像为关于原点中心对称
导数图像:当输入接近0时,tanh函数的导数接近最大值1。 与我们在sigmoid函数图像中看到的类似, 输入在任一方向上越远离0点,导数越接近0。
优点:
- tanh函数将输入值压缩到 -1~1 的范围,因此它是0均值的,解决了Sigmoid函数的非zero-centered问题
缺点:
- 存在梯度消失和幂运算的问题(梯度饱和与exp计算的问题)。
二:多层感知机从零开始实现
导入数据集->初始化参数->激活函数->损失函数->训练->预测
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)
print(test_iter,train_iter)
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]
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')
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)
d2l.predict_ch3(net, test_iter)
d2l.plt.show()
对reduction的解释
nn.CrossEntropyLoss
的reduction
参数控制损失值的聚合方式,它有几个可能的值:
'none'
或'sum_none'
:不聚合损失值。即,对于每个样本(或每个元素的损失,取决于输入的形状),损失函数将返回一个单独的损失值。这意味着返回的损失值将具有与输入样本数量相同的形状。'mean'
:返回损失值的平均值。这是最常见的设置,特别是在训练过程中,因为它给出了一个关于整个批次损失的单一标量值。'sum'
:返回损失值的总和。这在某些情况下很有用,但不如'mean'
那样普遍。当你设置
reduction='none'
时,你告诉 PyTorch 不要对损失值进行任何聚合。这意味着,如果你有一个批次的数据,并且每个样本都有一个损失值,那么nn.CrossEntropyLoss
将返回一个与批次中样本数量相同长度的张量,其中包含每个样本的损失值。这种设置有几个用途:
自定义聚合:你可能想要根据特定逻辑(比如加权损失)来自定义损失值的聚合方式。通过先获取每个样本的损失值,然后你可以根据需要对它们进行加权、平均或求和。
更细粒度的分析:在某些情况下,你可能想要对每个样本的损失值进行单独的分析,以了解模型在不同样本上的表现如何。
与其他损失函数结合:在训练多任务模型时,你可能想要将
nn.CrossEntropyLoss
的输出与其他任务的损失值结合起来。通过设置reduction='none'
,你可以更容易地实现这一点。
对SGD内容的分析
这段信息是关于PyTorch中SGD(随机梯度下降)优化器的一个参数组的配置说明。在PyTorch中,优化器可以管理多个参数组,每个参数组可以有自己的一套优化参数(如学习率、动量等)。然而,在你给出的这个例子中,似乎只展示了一个参数组的配置,且是以一种类似于字典或键值对的形式列出的。下面是对这些配置项的解释:
dampening
: 0
这个参数用于动量计算的“阻尼”,它有助于抑制动量项的振荡。值为0意味着不使用阻尼。
differentiable
: False
这个参数在PyTorch的官方SGD优化器文档中并不直接出现。它可能是一个非标准或特定于某个库/框架的扩展,或者是一个误解。在标准的PyTorch SGD优化器中,我们不会直接设置参数的“可微分性”,因为优化器本身不直接参与梯度计算(这是自动微分引擎的职责)。
foreach
: None
这个参数在PyTorch的SGD优化器中也不是标准的配置项。它可能指的是某种高级功能,用于对参数组中的每个参数应用不同的优化策略,但这并不是PyTorch标准SGD优化器的一部分。
lr
: 0.1
学习率(Learning Rate),是优化算法中的一个关键超参数,它决定了在优化过程中参数更新的步长大小。这里设置为0.1。
maximize
: False
这个参数通常用于指定优化问题的目标是最小化还是最大化。在PyTorch的SGD优化器中,默认是最小化损失函数,因此这个参数通常被设置为False。
momentum
: 0
动量(Momentum)是一种帮助加速SGD在相关方向上并抑制振荡的技术。动量项累积了之前梯度的指数衰减移动平均,并且继续沿该方向移动。这里设置为0意味着不使用动量。
nesterov
: False
Nesterov动量是对传统动量方法的一种改进,它在计算梯度之前先对参数进行了一个“预测”更新。这有助于加速收敛,并减少在某些情况下的振荡。这里设置为False意味着不使用Nesterov动量。
weight_decay
: 0
权重衰减(Weight Decay)是一种正则化技术,它通过向损失函数添加一个正则项来惩罚模型参数的规模,从而有助于防止过拟合。这个正则项是模型参数平方的系数乘以一个超参数(即权重衰减率)。这里设置为0意味着不使用权重衰减。综上所述,这个参数组的配置是一个相对简单的SGD优化器配置,其中只使用了学习率(lr=0.1),而没有使用动量、Nesterov动量、权重衰减等高级特性。
三:多层感知机简洁实现
定义net的时候我们有展平层,隐藏层,激活函数,输出层
import torch
from torch import nn
from d2l import torch as d2l
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);
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)
d2l.plt.show()
四:模型选择,欠拟合和过拟合
误差:泛化误差,训练误差
正常我们训练得出来的预测值与标准值之间的误差就是训练误差
而面对未知的测试集得出来的预测值与标准值之间的误差叫做泛化误差
对未知的反馈越好,泛化能力就越强
以下是影响泛化能力的因素:
-
可调整参数的数量。当可调整参数的数量(有时称为自由度)很大时,模型往往更容易过拟合。
-
参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。
-
训练样本的数量。即使模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。
验证集:
1.我们把样本分为三份,训练,测试和验证,因为现实中我们不会使用测试集一次就丢弃,但还要考虑泛化误差,所以验证集就应运而生了
2.k折交叉验证:把原始训练数据被分成
标签:loss,教程,函数,torch,感知机,train,d2l,足矣,net From: https://blog.csdn.net/Q268191051011/article/details/140584289