2024.01.12 多层感知机和常见激活函数
说明:2024.01.10没有学习,干活去了。。。
说明:2024.01.11没有学习,干活去了。。。
因为学习大模型的微调遇到了障碍,所以回头再快速看一遍深度学习的原理。都是一些基础内容。。。
https://zh.d2l.ai/chapter_multilayer-perceptrons/mlp.html
多层感知机原理
多层感知机的提出是因为并不是所有的关系都可以使用线性模型表达的,这很容易理解,线性关系在日常生活中毕竟还是少数。
假设我们的数据可能会有一种表示,这种表示会考虑到我们在特征之间的相关交互作用。 在此表示的基础上建立一个线性模型可能会是合适的, 但我们不知道如何手动计算这么一种表示。 对于深度神经网络,我们使用观测数据来联合学习隐藏层表示和应用于该表示的线性预测器。
隐藏层
我们可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制, 使其能处理更普遍的函数关系类型。 要做到这一点,最简单的方法是将许多全连接层堆叠在一起。 每一层都输出到上面的层,直到生成最后的输出。 我们可以把前 L − 1 L - 1 L−1层看作表示,把最后一层看作线性预测器。 这种架构通常称为多层感知机(multilayer perceptron),通常缩写为MLP。 下面,我们以图的方式描述了多层感知机。
这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。 输入层不涉及任何计算,因此使用此网络产生输出只需要实现隐藏层和输出层的计算。 因此,这个多层感知机中的层数为2。 注意,这两个层都是全连接的。 每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。
从线性到非线性转换
同之前的章节一样, 我们通过矩阵
X
∈
R
n
×
d
\mathbf{X}\in\mathbb{R}^{n\times d}
X∈Rn×d来表示
n
n
n个样本的小批量, 其中每个样本具有
d
d
d个输入特征。 对于具有
h
h
h个隐藏单元的单隐藏层多层感知机, 用
H
∈
R
n
×
h
\mathbf{H}\in\mathbb{R}^{n\times h}
H∈Rn×h表示隐藏层的输出, 称为隐藏表示(hidden representations)。 在数学或代码中,
H
\mathbf{H}
H也被称为隐藏层变量(hidden-layer variable) 或隐藏变量(hidden variable)。 因为隐藏层和输出层都是全连接的, 所以我们有隐藏层权重
W
(
1
)
∈
R
d
×
h
\mathbf{W}^{(1)}\in\mathbb{R}^{d\times h}
W(1)∈Rd×h和隐藏层偏置
b
(
1
)
∈
R
1
×
h
\mathbf{b}^{(1)}\in\mathbb{R}^{1\times h}
b(1)∈R1×h以及输出层权重
W
(
2
)
∈
R
h
×
q
\mathbf{W}^{(2)}\in\mathbb{R}^{h\times q}
W(2)∈Rh×q和输出层偏置
b
(
2
)
∈
R
1
×
q
\mathbf{b}^{(2)}\in\mathbb{R}^{1\times q}
b(2)∈R1×q。 形式上,我们按如下方式计算单隐藏层多层感知机的输出
O
∈
R
n
×
q
\mathbf{O}\in\mathbb{R}^{n\times q}
O∈Rn×q:
H
=
X
W
(
1
)
+
b
(
1
)
,
O
=
H
W
(
2
)
+
b
(
2
)
.
\begin{gathered}\mathbf{H}=\mathbf{X}\mathbf{W}^{(1)}+\mathbf{b}^{(1)},\\\mathbf{O}=\mathbf{H}\mathbf{W}^{(2)}+\mathbf{b}^{(2)}.\end{gathered}
H=XW(1)+b(1),O=HW(2)+b(2).
注意在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。 可我们能从中得到什么好处呢?在上面定义的模型里,我们没有好处! 原因很简单:上面的隐藏单元由输入的仿射函数给出, 而输出(softmax操作前)只是隐藏单元的仿射函数。 仿射函数的仿射函数本身就是仿射函数, 但是我们之前的线性模型已经能够表示任何仿射函数。我们可以证明这一等价性,即对于任意权重值, 我们只需合并隐藏层,便可产生具有参数
W
=
W
(
1
)
W
(
2
)
\mathbf{W}=\mathbf{W}^{(1)}\mathbf{W}^{(2)}
W=W(1)W(2)和
b
=
b
(
1
)
W
(
2
)
+
b
(
2
)
\mathbf{b}=\mathbf{b}^{(1)}\mathbf{W}^{(2)}+\mathbf{b}^{(2)}
b=b(1)W(2)+b(2)的等价单层模型:
O
=
(
X
W
(
1
)
+
b
(
1
)
)
W
(
2
)
+
b
(
2
)
=
X
W
(
1
)
W
(
2
)
+
b
(
1
)
W
(
2
)
+
b
(
2
)
=
X
W
+
b
\mathbf{O}=(\mathbf{X}\mathbf{W}^{(1)}+\mathbf{b}^{(1)})\mathbf{W}^{(2)}+\mathbf{b}^{(2)}=\mathbf{X}\mathbf{W}^{(1)}\mathbf{W}^{(2)}+\mathbf{b}^{(1)}\mathbf{W}^{(2)}+\mathbf{b}^{(2)}=\mathbf{X}\mathbf{W}+\mathbf{b}
O=(XW(1)+b(1))W(2)+b(2)=XW(1)W(2)+b(1)W(2)+b(2)=XW+b
上面说了这么多,主要想表达的是,虽然加入了隐藏层,但是,依旧与线性函数没啥区别,还是线性关系。
为了解决这个问题,使得模型发挥更大的潜力,我们还需要一个额外的关键要素: 在仿射变换之后对每个隐藏单元应用非线性的激活函数(activation function) σ \sigma σ:
激活函数的输出(例如,
σ
(
⋅
)
\sigma(\cdot)
σ(⋅))被称为活性值(activations)。 一般来说,有了激活函数,就不可能再将我们的多层感知机退化成线性模型:
H
=
σ
(
X
W
(
1
)
+
b
(
1
)
)
,
O
=
H
W
(
2
)
+
b
(
2
)
.
\begin{aligned}\mathbf{H}&=\sigma(\mathbf{X}\mathbf{W}^{(1)}+\mathbf{b}^{(1)}),\\\mathbf{O}&=\mathbf{H}\mathbf{W}^{(2)}+\mathbf{b}^{(2)}.\end{aligned}
HO=σ(XW(1)+b(1)),=HW(2)+b(2).
由于
X
\mathbf{X}
X中的每一行对应于小批量中的一个样本, 出于记号习惯的考量, 我们定义非线性函数
σ
\sigma
σ也以按行的方式作用于其输入, 即一次计算一个样本。 但是本节应用于隐藏层的激活函数通常不仅按行操作,也按元素操作。这意味着在计算每一层的线性部分之后,我们可以计算每个活性值, 而不需要查看其他隐藏单元所取的值。对于大多数激活函数都是这样。
为了构建更通用的多层感知机, 我们可以继续堆叠这样的隐藏层, 例如 H ( 1 ) = σ 1 ( X W ( 1 ) + b ( 1 ) ) \mathbf{H}^{(1)}=\sigma_1(\mathbf{X}\mathbf{W}^{(1)}+\mathbf{b}^{(1)}) H(1)=σ1(XW(1)+b(1))和 H ( 2 ) = σ 2 ( H ( 1 ) W ( 2 ) + b ( 2 ) ) \mathbf{H}^{(2)}=\sigma_2(\mathbf{H}^{(1)}\mathbf{W}^{(2)}+\mathbf{b}^{(2)}) H(2)=σ2(H(1)W(2)+b(2)), 一层叠一层,从而产生更有表达能力的模型。
通用近似定理
多层感知机可以通过隐藏神经元,捕捉到输入之间复杂的相互作用, 这些神经元依赖于每个输入的值。 我们可以很容易地设计隐藏节点来执行任意计算。 例如,在一对输入上进行基本逻辑操作,多层感知机是通用近似器。 即使是网络只有一个隐藏层,给定足够的神经元和正确的权重, 我们可以对任意函数建模,尽管实际中学习该函数是很困难的。 神经网络有点像C语言。 C语言和任何其他现代编程语言一样,能够表达任何可计算的程序。 但实际上,想出一个符合规范的程序才是最困难的部分。
而且,虽然一个单隐层网络能学习任何函数, 但并不意味着我们应该尝试使用单隐藏层网络来解决所有问题。 事实上,通过使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。 我们将在后面的章节中进行更细致的讨论。
激活函数
激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活, 它们将输入信号转换为输出的可微运算。 大多数激活函数都是非线性的。 由于激活函数是深度学习的基础,下面简要介绍一些常见的激活函数。
ReLU函数
最受欢迎的激活函数是修正线性单元(Rectified linear unit,ReLU), 因为它实现简单,同时在各种预测任务中表现良好。 ReLU提供了一种非常简单的非线性变换。 给定元素
x
x
x,ReLU函数被定义为该元素与0的最大值:
R
e
L
U
(
x
)
=
max
(
x
,
0
)
\mathrm{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)
当输入为负时,ReLU函数的导数为0,而当输入为正时,ReLU函数的导数为1。 注意,当输入值精确等于0时,ReLU函数不可导。 在此时,我们默认使用左侧的导数,即当输入为0时导数为0。
relu函数的导数图:
使用ReLU的原因是,它求导表现得特别好:要么让参数消失,要么让参数通过。 这使得优化表现得更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题(稍后将详细介绍)。
sigmoid函数
对于一个定义域在
R
\mathbb{R}
R中的输入, sigmoid函数将输入变换为区间(0, 1)上的输出。 因此,sigmoid通常称为挤压函数(squashing function): 它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值:
sigmoid
(
x
)
=
1
1
+
exp
(
−
x
)
\operatorname{sigmoid}(x)=\frac1{1+\exp(-x)}
sigmoid(x)=1+exp(−x)1
当我们想要将输出视作二元分类问题的概率时, sigmoid仍然被广泛用作输出单元上的激活函数 (sigmoid可以视为softmax的特例)。 然而,sigmoid在隐藏层中已经较少使用, 它在大部分时候被更简单、更容易训练的ReLU所取代。
y = torch.sigmoid(x)
sigmoid函数的导数如下:
sigmoid函数的导数如下:
d
d
x
sigmoid
(
x
)
=
exp
(
−
x
)
(
1
+
exp
(
−
x
)
)
2
=
sigmoid
(
x
)
(
1
−
sigmoid
(
x
)
)
\frac d{dx}\text{sigmoid}(x)=\frac{\exp(-x)}{(1+\exp(-x))^2}=\text{sigmoid}(x)\left(1-\text{sigmoid}(x)\right)
dxdsigmoid(x)=(1+exp(−x))2exp(−x)=sigmoid(x)(1−sigmoid(x))
注意,当输入为0时,sigmoid函数的导数达到最大值0.25; 而输入在任一方向上越远离0点时,导数越接近0。sigmoid函数的导数图像如下所示。
tanh函数
与sigmoid函数类似, tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上。 tanh函数的公式如下:
tanh
(
x
)
=
1
−
exp
(
−
2
x
)
1
+
exp
(
−
2
x
)
\tanh(x)=\frac{1-\exp(-2x)}{1+\exp(-2x)}
tanh(x)=1+exp(−2x)1−exp(−2x)
下面我们绘制tanh函数。 注意,当输入在0附近时,tanh函数接近线性变换。 函数的形状类似于sigmoid函数, 不同的是tanh函数关于坐标系原点中心对称。
y = torch.tanh(x)
tanh函数图像:
tanh函数的导数是:
d
d
x
t
a
n
h
(
x
)
=
1
−
t
a
n
h
2
(
x
)
\frac d{dx}\mathrm{tanh}(x)=1-\mathrm{tanh}^2(x)
dxdtanh(x)=1−tanh2(x)
当输入接近0时,tanh函数的导数接近最大值1。 与我们在sigmoid函数图像中看到的类似, 输入在任一方向上越远离0点,导数越接近0。tanh函数的导数图像如下所示。
多层感知机的从零开始实现
1 加载数据集
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
#通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,并除以255使得所有像素的数值均在0~1之间
trans = [transforms.ToTensor()]
if resize:
# transforms.Resize(256)调整图像大小到 256x256 像素
trans.insert(0, transforms.Resize(resize))
#当你需要对图像进行一系列预处理步骤时,transforms.Compose 允许你以一种简洁和模块化的方式来堆叠这些变换。
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=4),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=4))
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)
2 初始化模型参数
import torch
from torch import nn
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]
3 定义relu激活函数
def relu(X):
a = torch.zeros_like(X)
return torch.max(X, a)
4 多层感知机模型
def net(X):
X = X.reshape((-1, num_inputs))
H = relu(X@W1 + b1)
return (H@W2 + b2)
5 定义损失函数
loss = nn.CrossEntropyLoss(reduction='none')
6 定义优化算法
lr = 0.1
updater = torch.optim.SGD(params, lr=lr)
7 定义准确率计算函数
def accuracy(y_hat, y): #@save
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())
class Accumulator: #@save
"""在n个变量上累加"""
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]
def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 正确预测数、预测总数
with torch.no_grad():
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
8 定义模型训练函数
def train_epoch_ch3(net, train_iter, loss, updater):
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
# print(y_hat)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad()
l.mean().backward()
updater.step()
else:
# 使用定制的优化器和损失函数
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
9 训练
num_epochs = 10
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
test_acc = evaluate_accuracy(net, test_iter)
print(epoch, train_metrics, test_acc)
输出:
0 (1.0446819251378376, 0.6393666666666666) 0.7315
1 (0.6017857405344645, 0.7879333333333334) 0.7971
2 (0.5207550083796183, 0.8179333333333333) 0.8157
3 (0.48017177867889405, 0.8309833333333333) 0.8221
4 (0.45705518697102865, 0.8393666666666667) 0.8239
5 (0.43581265424092613, 0.84595) 0.8239
6 (0.42090332431793215, 0.8512333333333333) 0.8274
7 (0.4069797029495239, 0.8567666666666667) 0.8463
8 (0.3946374579111735, 0.8606666666666667) 0.8412
9 (0.3835021224975586, 0.86425) 0.8437
多层感知机的简洁实现
1 加载数据集
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
def load_data_fashion_mnist(batch_size, resize=None): #@save
"""下载Fashion-MNIST数据集,然后将其加载到内存中"""
#通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,并除以255使得所有像素的数值均在0~1之间
trans = [transforms.ToTensor()]
if resize:
# transforms.Resize(256)调整图像大小到 256x256 像素
trans.insert(0, transforms.Resize(resize))
#当你需要对图像进行一系列预处理步骤时,transforms.Compose 允许你以一种简洁和模块化的方式来堆叠这些变换。
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=4),
data.DataLoader(mnist_test, batch_size, shuffle=False,
num_workers=4))
batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)
2 定义多层感知机模型和激活函数并初始化参数
from torch import nn
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)
3 定义损失函数/优化算法
lr = 0.1
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)
4 定义准确率计算函数
def accuracy(y_hat, y): #@save
"""计算预测正确的数量"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1)
cmp = y_hat.type(y.dtype) == y
return float(cmp.type(y.dtype).sum())
class Accumulator: #@save
"""在n个变量上累加"""
def __init__(self, n):
self.data = [0.0] * n
def add(self, *args):
self.data = [a + float(b) for a, b in zip(self.data, args)]
def reset(self):
self.data = [0.0] * len(self.data)
def __getitem__(self, idx):
return self.data[idx]
def evaluate_accuracy(net, data_iter): #@save
"""计算在指定数据集上模型的精度"""
if isinstance(net, torch.nn.Module):
net.eval() # 将模型设置为评估模式
metric = Accumulator(2) # 正确预测数、预测总数
with torch.no_grad():
for X, y in data_iter:
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1]
5 定义模型训练函数
def train_epoch_ch3(net, train_iter, loss, updater):
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
# print(y_hat)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad()
l.mean().backward()
updater.step()
else:
# 使用定制的优化器和损失函数
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
6 训练
num_epochs = 10
for epoch in range(num_epochs):
train_metrics = train_epoch_ch3(net, train_iter, loss, trainer)
test_acc = evaluate_accuracy(net, test_iter)
print(epoch, train_metrics, test_acc)
输出:
0 (1.0455598266601562, 0.6397833333333334) 0.7102
1 (0.6025627362569174, 0.7862833333333333) 0.7905
2 (0.5176767019907633, 0.81885) 0.8039
3 (0.48023186473846435, 0.8307833333333333) 0.8167
4 (0.45216845607757566, 0.8416) 0.8141
5 (0.4324684030532837, 0.8473166666666667) 0.8327
6 (0.41943558101654055, 0.8524166666666667) 0.8113
7 (0.4035598129272461, 0.8574833333333334) 0.8349
8 (0.3925578647931417, 0.8604) 0.821
9 (0.3817426620165507, 0.8658333333333333) 0.8403