五、 激活函数
激活函数的作用是在隐藏层引入非线性,使得神经网络能够学习和表示复杂的函数关系,使网络具备非线性能力,增强其表达能力。
1. 基础概念
通过认识线性和非线性的基础概念,深刻理解激活函数存在的价值。
1.1 非线性理解
如果在隐藏层不使用激活函数,那么整个神经网络会表现为一个线性模型。我们可以通过数学推导来展示这一点。
假设:
- 神经网络有 L L L 层,每层的输出为 a ( l ) \mathbf{a}^{(l)} a(l)。
- 每层的权重矩阵为 W ( l ) \mathbf{W}^{(l)} W(l),偏置向量为 b ( l ) \mathbf{b}^{(l)} b(l)。
- 输入数据为 x \mathbf{x} x,输出为 a ( L ) \mathbf{a}^{(L)} a(L)。
一层网络的情况
对于单层网络(输入层到输出层),如果没有激活函数,输出
a
(
1
)
\mathbf{a}^{(1)}
a(1) 可以表示为:
a
(
1
)
=
W
(
1
)
x
+
b
(
1
)
\mathbf{a}^{(1)} = \mathbf{W}^{(1)} \mathbf{x} + \mathbf{b}^{(1)}
a(1)=W(1)x+b(1)
两层网络的情况
假设我们有两层网络,且每层都没有激活函数,则:
- 第一层的输出: a ( 1 ) = W ( 1 ) x + b ( 1 ) \mathbf{a}^{(1)} = \mathbf{W}^{(1)} \mathbf{x} + \mathbf{b}^{(1)} a(1)=W(1)x+b(1)
- 第二层的输出: a ( 2 ) = W ( 2 ) a ( 1 ) + b ( 2 ) \mathbf{a}^{(2)} = \mathbf{W}^{(2)} \mathbf{a}^{(1)} + \mathbf{b}^{(2)} a(2)=W(2)a(1)+b(2)
将 a ( 1 ) \mathbf{a}^{(1)} a(1)代入到 a ( 2 ) \mathbf{a}^{(2)} a(2)中,可以得到:
a ( 2 ) = W ( 2 ) ( W ( 1 ) x + b ( 1 ) ) + b ( 2 ) \mathbf{a}^{(2)} = \mathbf{W}^{(2)} (\mathbf{W}^{(1)} \mathbf{x} + \mathbf{b}^{(1)}) + \mathbf{b}^{(2)} a(2)=W(2)(W(1)x+b(1))+b(2)
a ( 2 ) = W ( 2 ) W ( 1 ) x + W ( 2 ) b ( 1 ) + b ( 2 ) \mathbf{a}^{(2)} = \mathbf{W}^{(2)} \mathbf{W}^{(1)} \mathbf{x} + \mathbf{W}^{(2)} \mathbf{b}^{(1)} + \mathbf{b}^{(2)} a(2)=W(2)W(1)x+W(2)b(1)+b(2)
我们可以看到,输出
a
(
2
)
\mathbf{a}^{(2)}
a(2)是输入
x
\mathbf{x}
x的线性变换,因为:
a
(
2
)
=
W
′
x
+
b
′
\mathbf{a}^{(2)} = \mathbf{W}' \mathbf{x} + \mathbf{b}'
a(2)=W′x+b′
其中
W
′
=
W
(
2
)
W
(
1
)
\mathbf{W}' = \mathbf{W}^{(2)} \mathbf{W}^{(1)}
W′=W(2)W(1),
b
′
=
W
(
2
)
b
(
1
)
+
b
(
2
)
\mathbf{b}' = \mathbf{W}^{(2)} \mathbf{b}^{(1)} + \mathbf{b}^{(2)}
b′=W(2)b(1)+b(2)。
多层网络的情况
如果有 L L L层,每层都没有激活函数,则第 l l l层的输出为: a ( l ) = W ( l ) a ( l − 1 ) + b ( l ) \mathbf{a}^{(l)} = \mathbf{W}^{(l)} \mathbf{a}^{(l-1)} + \mathbf{b}^{(l)} a(l)=W(l)a(l−1)+b(l)
通过递归代入,可以得到:
a
(
L
)
=
W
(
L
)
W
(
L
−
1
)
⋯
W
(
1
)
x
+
W
(
L
)
W
(
L
−
1
)
⋯
W
(
2
)
b
(
1
)
+
W
(
L
)
W
(
L
−
1
)
⋯
W
(
3
)
b
(
2
)
+
⋯
+
b
(
L
)
\mathbf{a}^{(L)} = \mathbf{W}^{(L)} \mathbf{W}^{(L-1)} \cdots \mathbf{W}^{(1)} \mathbf{x} + \mathbf{W}^{(L)} \mathbf{W}^{(L-1)} \cdots \mathbf{W}^{(2)} \mathbf{b}^{(1)} + \mathbf{W}^{(L)} \mathbf{W}^{(L-1)} \cdots \mathbf{W}^{(3)} \mathbf{b}^{(2)} + \cdots + \mathbf{b}^{(L)}
a(L)=W(L)W(L−1)⋯W(1)x+W(L)W(L−1)⋯W(2)b(1)+W(L)W(L−1)⋯W(3)b(2)+⋯+b(L)
表达式可简化为:
a
(
L
)
=
W
′
′
x
+
b
′
′
\mathbf{a}^{(L)} = \mathbf{W}'' \mathbf{x} + \mathbf{b}''
a(L)=W′′x+b′′
其中,
W
′
′
\mathbf{W}''
W′′ 是所有权重矩阵的乘积,
b
′
′
\mathbf{b}''
b′′是所有偏置项的线性组合。
如此可以看得出来,无论网络多少层,意味着:
整个网络就是线性模型,无法捕捉数据中的非线性关系。
激活函数是引入非线性特性、使神经网络能够处理复杂问题的关键。
1.2 非线性可视化
我们可以通过可视化的方式去理解非线性的拟合能力:https://playground.tensorflow.org/
2. 常见激活函数
激活函数通过引入非线性来增强神经网络的表达能力,对于解决线性模型的局限性至关重要。由于反向传播算法(BP)用于更新网络参数,因此激活函数必须是可微的,也就是说能够求导的。
2.1 sigmoid
Sigmoid激活函数是一种常见的非线性激活函数,特别是在早期神经网络中应用广泛。它将输入映射到0到1之间的值,因此非常适合处理概率问题。
2.1.1 公式
Sigmoid函数的数学表达式为:
f
(
x
)
=
σ
(
x
)
=
1
1
+
e
−
x
f(x) = \sigma(x) = \frac{1}{1 + e^{-x}}
f(x)=σ(x)=1+e−x1
其中,
e
e
e 是自然常数(约等于2.718),
x
x
x 是输入。
2.1.2 特征
-
将任意实数输入映射到 (0, 1)之间,因此非常适合处理概率场景。
-
sigmoid函数一般只用于二分类的输出层。
-
微分性质: 导数计算比较方便,可以用自身表达式来表示:
σ ′ ( x ) = σ ( x ) ⋅ ( 1 − σ ( x ) ) \sigma'(x)=\sigma(x)\cdot(1-\sigma(x)) σ′(x)=σ(x)⋅(1−σ(x))
2.1.3 缺点
- 梯度消失:
- 在输入非常大或非常小时,Sigmoid函数的梯度会变得非常小,接近于0。这导致在反向传播过程中,梯度逐渐衰减。
- 最终使得早期层的权重更新非常缓慢,进而导致训练速度变慢甚至停滞。
- 信息丢失:输入100和输入10000经过sigmoid的激活值几乎都是等于 1 的,但是输入的数据却相差 100 倍。
- 计算成本高: 由于涉及指数运算,Sigmoid的计算比ReLU等函数更复杂,尽管差异并不显著。
2.1.4 函数绘制
通过代码实现函数和导函数绘制:
import torch
import matplotlib.pyplot as plt
# plt支持中文
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
def test001():
# 一行两列绘制图像
_, ax = plt.subplots(1, 2)
# 绘制函数图像
x = torch.linspace(-10, 10, 100)
y = torch.sigmoid(x)
# 网格
ax[0].grid(True)
ax[0].set_title("sigmoid 函数曲线图")
ax[0].set_xlabel("x")
ax[0].set_ylabel("y")
# 在第一行第一列绘制sigmoid函数曲线图
ax[0].plot(x, y)
# 绘制sigmoid导数曲线图
x = torch.linspace(-10, 10, 100, requires_grad=True)
# y = torch.sigmoid(x) * (1 - torch.sigmoid(x))
# 自动求导
torch.sigmoid(x).sum().backward()
ax[1].grid(True)
ax[1].set_title("sigmoid 函数导数曲线图", color="red")
ax[1].set_xlabel("x")
ax[1].set_ylabel("y")
# ax[1].plot(x.detach().numpy(), y.detach())
# 用自动求导的结果绘制曲线图
ax[1].plot(x.detach().numpy(), x.grad.detach().numpy())
# 设置曲线颜色
ax[1].lines[0].set_color("red")
plt.show()
if __name__ == "__main__":
test001()
运行结果:
2.2 tanh
tanh(双曲正切)是一种常见的非线性激活函数,常用于神经网络的隐藏层。tanh 函数也是一种S形曲线,输出范围为 ( − 1 , 1 ) (−1,1) (−1,1)。
2.2.1 公式
tanh数学表达式为:
t
a
n
h
(
x
)
=
e
x
−
e
−
x
e
x
+
e
−
x
{tanh}(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}}
tanh(x)=ex+e−xex−e−x
2.2.2 特征
-
输出范围: 将输入映射到 ( − 1 , 1 ) (-1, 1) (−1,1)之间,因此输出是零中心的。相比于Sigmoid函数,这种零中心化的输出有助于加速收敛。
-
对称性: Tanh函数关于原点对称,因此在输入为0时,输出也为0。这种对称性有助于在训练神经网络时使数据更平衡。
-
平滑性: Tanh函数在整个输入范围内都是连续且可微的,这使其非常适合于使用梯度下降法进行优化。
d d x tanh ( x ) = 1 − tanh 2 ( x ) \frac{d}{dx} \text{tanh}(x) = 1 - \text{tanh}^2(x) dxdtanh(x)=1−tanh2(x)
2.2.3 缺点
- 梯度消失: 虽然一定程度上改善了梯度消失问题,但在输入值非常大或非常小时导数还是非常小,这在深层网络中仍然是个问题。
- 计算成本: 由于涉及指数运算,Tanh的计算成本还是略高,尽管差异不大。
2.2.4 函数绘制
绘制代码:
import torch
import matplotlib.pyplot as plt
# plt支持中文
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
def test001():
# 一行两列绘制图像
_, ax = plt.subplots(1, 2)
# 绘制函数图像
x = torch.linspace(-10, 10, 100)
y = torch.tanh(x)
# 网格
ax[0].grid(True)
ax[0].set_title("tanh 函数曲线图")
ax[0].set_xlabel("x")
ax[0].set_ylabel("y")
# 在第一行第一列绘制tanh函数曲线图
ax[0].plot(x, y)
# 绘制tanh导数曲线图
x = torch.linspace(-10, 10, 100, requires_grad=True)
# y = torch.tanh(x) * (1 - torch.tanh(x))
# 自动求导:需要标量才能反向传播
torch.tanh(x).sum().backward()
ax[1].grid(True)
ax[1].set_title("tanh 函数导数曲线图", color="red")
ax[1].set_xlabel("x")
ax[1].set_ylabel("x.grad")
# ax[1].plot(x.detach().numpy(), y.detach())
# 用自动求导的结果绘制曲线图
ax[1].plot(x.detach().numpy(), x.grad.detach().numpy())
# 设置曲线颜色
ax[1].lines[0].set_color("red")
plt.show()
if __name__ == "__main__":
test001()
绘制结果:
2.3 ReLU
ReLU(Rectified Linear Unit)是深度学习中最常用的激活函数之一,它的全称是修正线性单元。ReLU 激活函数的定义非常简单,但在实践中效果非常好。
2.3.1 公式
ReLU 函数定义如下:
ReLU
(
x
)
=
max
(
0
,
x
)
\text{ReLU}(x) = \max(0, x)
ReLU(x)=max(0,x)
即
R
e
L
U
ReLU
ReLU对输入
x
x
x进行非线性变换:
∙
当
x
>
0
时,ReLU
(
x
)
=
x
∙
当
x
≤
0
时,ReLU
(
x
)
=
0
\bullet\quad\text{当 }x>0\text{ 时,ReLU}(x)=x\text{}\\\bullet\quad\text{当 }x\leq0\text{ 时,ReLU}(x)=0\text{}
∙当 x>0 时,ReLU(x)=x∙当 x≤0 时,ReLU(x)=0
2.3.2 特征
-
计算简单:ReLU 的计算非常简单,只需要对输入进行一次比较运算,这在实际应用中大大加速了神经网络的训练。
-
ReLU 函数的导数是分段函数:
ReLU ′ ( x ) = { 1 , if x > 0 0 , if x ≤ 0 \text{ReLU}'(x)=\begin{cases}1,&\text{if } x>0\\0,&\text{if }x\leq0\end{cases} ReLU′(x)={1,0,if x>0if x≤0 -
缓解梯度消失问题:相比于 Sigmoid 和 Tanh 激活函数,ReLU 在正半区的导数恒为 1,这使得深度神经网络在训练过程中可以更好地传播梯度,不存在饱和问题。
-
稀疏激活:ReLU在输入小于等于 0 时输出为 0,这使得 ReLU 可以在神经网络中引入稀疏性(即一些神经元不被激活),这种稀疏性可以提升网络的泛化能力。
2.3.3 缺点
神经元死亡:由于 R e L U ReLU ReLU在 x ≤ 0 x≤0 x≤0时输出为 0 0 0,如果某个神经元输入值是负,那么该神经元将永远不再激活,成为“死亡”神经元。随着训练的进行,网络中可能会出现大量死亡神经元,从而会降低模型的表达能力。
2.3.4 函数绘图
参考代码如下:
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
# 中文问题
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
def test006():
# 输入数据x
x = torch.linspace(-20, 20, 1000)
y = F.relu(x)
# 绘制一行2列
_, ax = plt.subplots(1, 2)
ax[0].plot(x.numpy(), y.numpy())
# 显示坐标格子
ax[0].grid()
ax[0].set_title("relu 激活函数")
ax[0].set_xlabel("x")
ax[0].set_ylabel("y")
# 绘制导数函数
x = torch.linspace(-20, 20, 1000, requires_grad=True)
F.relu(x).sum().backward()
ax[1].plot(x.detach().numpy(), x.grad.numpy())
ax[1].grid()
ax[1].set_title("relu 激活函数导数", color="red")
# 设置绘制线色颜色
ax[1].lines[0].set_color("red")
ax[1].set_xlabel("x")
ax[1].set_ylabel("x.grad")
plt.show()
if __name__ == "__main__":
test006()
执行结果如下:
2.4 LeakyReLU
Leaky ReLU是一种对 ReLU 函数的改进,旨在解决 ReLU 的一些缺点,特别是Dying ReLU 问题。Leaky ReLU 通过在输入为负时引入一个小的负斜率来改善这一问题。
2.4.1 公式
Leaky ReLU 函数的定义如下:
Leaky ReLU
(
x
)
=
{
x
,
if
x
>
0
α
x
,
if
x
≤
0
\text{Leaky ReLU}(x)=\begin{cases}x,&\text{if } x>0\\\alpha x,&\text{if } x\leq0\end{cases}
Leaky ReLU(x)={x,αx,if x>0if x≤0
其中,
α
\alpha
α 是一个非常小的常数(如 0.01),它控制负半轴的斜率。这个常数
α
\alpha
α是一个超参数,可以在训练过程中可自行进行调整。
2.4.2 特征
- 避免神经元死亡:通过在 x ≤ 0 x\leq 0 x≤0 区域引入一个小的负斜率,这样即使输入值小于等于零,Leaky ReLU仍然会有梯度,允许神经元继续更新权重,避免神经元在训练过程中完全“死亡”的问题。
- 计算简单:Leaky ReLU 的计算与 ReLU 相似,只需简单的比较和线性运算,计算开销低。
2.4.3 缺点
- 参数选择: α \alpha α 是一个需要调整的超参数,选择合适的 α \alpha α 值可能需要实验和调优。
- 出现负激活:如果 α \alpha α 设定得不当,仍然可能导致激活值过低。
2.4.4 函数绘制
参考代码:
import torch
import torch.nn.functional as F
import matplotlib.pyplot as plt
# 中文设置
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
def test006():
x = torch.linspace(-5, 5, 200)
# 设置leaky_relu的
slope = 0.03
y = F.leaky_relu(x, slope)
# 一行两列
_, ax = plt.subplots(1, 2)
# 开始绘制函数曲线图
ax[0].plot(x, y)
ax[0].set_title("Leaky ReLU 函数曲线图")
ax[0].set_xlabel("x")
ax[0].set_ylabel("y")
ax[0].grid(True)
# 绘制leaky_relu的梯度曲线图
x = torch.linspace(-5, 5, 200, requires_grad=True)
F.leaky_relu(x, slope).sum().backward()
ax[1].plot(x.detach().numpy(), x.grad)
ax[1].set_title("Leaky ReLU 梯度曲线图", color="red")
ax[1].set_xlabel("x")
ax[1].set_ylabel("x.grad")
ax[1].grid(True)
# 设置线的颜色
ax[1].lines[0].set_color("red")
plt.show()
if __name__ == "__main__":
test006()
运行结果:
2.5 softmax
Softmax激活函数通常用于分类问题的输出层,它能够将网络的输出转换为概率分布,使得输出的各个类别的概率之和为 1。Softmax 特别适合用于多分类问题。
2.5.1 公式
假设神经网络的输出层有
n
n
n个节点,每个节点的输出为
z
i
z_i
zi,则 Softmax 函数的定义如下:
S
o
f
t
m
a
x
(
z
i
)
=
e
z
i
∑
j
=
1
n
e
z
j
\mathrm{Softmax}(z_i)=\frac{e^{z_i}}{\sum_{j=1}^ne^{z_j}}
Softmax(zi)=∑j=1nezjezi
2.5.2 特征
-
将输出转化为概率:通过 S o f t m a x Softmax Softmax,可以将网络的原始输出转化为各个类别的概率,从而可以根据这些概率进行分类决策。
-
概率分布: S o f t m a x Softmax Softmax的输出是一个概率分布,即每个输出值 Softmax ( z i ) \text{Softmax}(z_i) Softmax(zi)都是一个介于 0 0 0和 1 1 1之间的数,并且所有输出值的和为 1:
∑ i = 1 n Softmax ( z i ) = 1 \sum_{i=1}^n\text{Softmax}(z_i)=1 i=1∑nSoftmax(zi)=1 -
突出差异: S o f t m a x Softmax Softmax会放大差异,使得概率最大的类别的输出值更接近 1 1 1,而其他类别更接近 0 0 0。
-
在实际应用中, S o f t m a x Softmax Softmax常与交叉熵损失函数 C r o s s − E n t r o p y L o s s Cross-Entropy Loss Cross−EntropyLoss结合使用,用于多分类问题。在反向传播中, S o f t m a x Softmax Softmax的导数计算是必需的。
设 p i = S o f t m a x ( z i ) ,则对于 z i 的导数为: ∙ 当 i = j 时: ∂ p i ∂ z i = p i ( 1 − p i ) ∙ 当 i ≠ j 时 : ∂ p i ∂ z j = − p i p j \begin{aligned} &\text{设 }p_i=\mathrm{Softmax}(z_i)\text{,则对于 }z_i\text{ 的导数为:} \\ &\bullet\text{ 当 }i=j\text{ 时:} \\ &&&\frac{\partial p_i}{\partial z_i}=p_i(1-p_i) \\ & \bullet\text{ 当 }i\neq j\text{ 时}: \\ &&&\frac{\partial p_i}{\partial z_j} =-p_{i}p_{j} \end{aligned} 设 pi=Softmax(zi),则对于 zi 的导数为:∙ 当 i=j 时:∙ 当 i=j 时:∂zi∂pi=pi(1−pi)∂zj∂pi=−pipj
2.5.3 缺点
- 数值不稳定性:在计算过程中,如果 z i z_i zi的数值过大, e z i e^{z_i} ezi可能会导致数值溢出。因此在实际应用中,经常会对 z i z_i zi进行调整,如减去最大值以确保数值稳定。
S o f t m a x ( z i ) = e z i − max ( z ) ∑ j = 1 n e z j − max ( z ) \mathrm{Softmax}(z_i)=\frac{e^{z_i-\max(z)}}{\sum_{j=1}^ne^{z_j-\max(z)}} Softmax(zi)=∑j=1nezj−max(z)ezi−max(z)
解释:
z i − max ( z ) z_i-\max(z) zi−max(z)是一个非正数,那么 e z i − max ( z ) e^{z_i - \max(z)} ezi−max(z)的值就位于 0 0 0到 1 1 1之间,有效避免了数值溢出。
这中调整不会改变 S o f t m a x Softmax Softmax的概率分布结果,因为从数学的角度讲相当于分子、分母都除以了 e max ( z ) e^{\max(z)} emax(z)。
- 难以处理大量类别: S o f t m a x Softmax Softmax在处理类别数非常多的情况下(如大模型中的词汇表)计算开销会较大。
2.5.4 代码实现
代码参考如下:
import torch
import torch.nn as nn
# 表示4分类,每个样本全连接后得到4个得分,下面示例模拟的是两个样本的得分
input_tensor = torch.tensor([[-1.0, 2.0, -3.0, 4.0], [-2, 3, -3, 9]])
softmax = nn.Softmax()
output_tensor = softmax(input_tensor)
# 关闭科学计数法
torch.set_printoptions(sci_mode=False)
print("输入张量:", input_tensor)
print("输出张量:", output_tensor)
输出结果:
输入张量: tensor([[-1., 2., -3., 4.],
[-2., 3., -3., 9.]])
输出张量: tensor([[ 0.0059, 0.1184, 0.0008, 0.8749],
[ 0.0000, 0.0025, 0.0000, 0.9975]])
3. 如何选择
更多激活函数可以查看官方文档:https://pytorch.org/docs/stable/nn.html#non-linear-activations-weighted-sum-nonlinearity
那这么多激活函数应该如何选择呢?实际没那么纠结
3.1 隐藏层
- 优先选ReLU;
- 如果ReLU效果不咋地,那么尝试其他激活,如Leaky ReLU等;
- 使用ReLU时注意神经元死亡问题, 避免出现过多神经元死亡;
- 不使用sigmoid,尝试使用tanh;
3.2 输出层
- 二分类问题选择sigmoid激活函数;
- 多分类问题选择softmax激活函数;
- 回归问题选择identity激活函数;
六、损失函数
1. 线性回归损失函数
1.1 MAE损失
MAE(Mean Absolute Error,平均绝对误差)通常也被称为 L1-Loss,通过对预测值和真实值之间的绝对差取平均值来衡量他们之间的差异。。
MAE的公式如下:
MAE
=
1
n
∑
i
=
1
n
∣
y
i
−
y
^
i
∣
\text{MAE} = \frac{1}{n} \sum_{i=1}^{n} \left| y_i - \hat{y}_i \right|
MAE=n1i=1∑n∣yi−y^i∣
其中:
- n n n 是样本的总数。
- y i y_i yi 是第 i i i 个样本的真实值。
- y ^ i \hat{y}_i y^i 是第 i i i 个样本的预测值。
- ∣ y i − y ^ i ∣ \left| y_i - \hat{y}_i \right| ∣yi−y^i∣ 是真实值和预测值之间的绝对误差。
特点:
- 鲁棒性:与均方误差(MSE)相比,MAE对异常值(outliers)更为鲁棒,因为它不会像MSE那样对较大误差平方敏感。
- 物理意义直观:MAE以与原始数据相同的单位度量误差,使其易于解释。
应用场景:
MAE通常用于需要对误差进行线性度量的情况,尤其是当数据中可能存在异常值时,MAE可以避免对异常值的过度惩罚。
使用torch.nn.L1Loss
即可计算MAE:
import torch
import torch.nn as nn
# 初始化MAE损失函数
mae_loss = nn.L1Loss()
# 假设 y_true 是真实值, y_pred 是预测值
y_true = torch.tensor([3.0, 5.0, 2.5])
y_pred = torch.tensor([2.5, 5.0, 3.0])
# 计算MAE
loss = mae_loss(y_pred, y_true)
print(f'MAE Loss: {loss.item()}')
1.2 MSE损失
均方差损失,也叫L2Loss。
MSE(Mean Squared Error,均方误差)通过对预测值和真实值之间的误差平方取平均值,来衡量预测值与真实值之间的差异。
MSE的公式如下:
MSE
=
1
n
∑
i
=
1
n
(
y
i
−
y
^
i
)
2
\text{MSE} = \frac{1}{n} \sum_{i=1}^{n} \left( y_i - \hat{y}_i \right)^2
MSE=n1i=1∑n(yi−y^i)2
其中:
- n n n 是样本的总数。
- y i y_i yi 是第 i i i 个样本的真实值。
- y ^ i \hat{y}_i y^i 是第 i i i 个样本的预测值。
- ( y i − y ^ i ) 2 \left( y_i - \hat{y}_i \right)^2 (yi−y^i)2 是真实值和预测值之间的误差平方。
特点:
- 平方惩罚:因为误差平方,MSE 对较大误差施加更大惩罚,所以 MSE 对异常值更为敏感。
- 凸性:MSE 是一个凸函数,这意味着它具有一个唯一的全局最小值,有助于优化问题的求解。
应用场景:
MSE被广泛应用在神经网络中。
使用 torch.nn.MSELoss
可以实现:
import torch
import torch.nn as nn
# 初始化MSE损失函数
mse_loss = nn.MSELoss()
# 假设 y_true 是真实值, y_pred 是预测值
y_true = torch.tensor([3.0, 5.0, 2.5])
y_pred = torch.tensor([2.5, 5.0, 3.0])
# 计算MSE
loss = mse_loss(y_pred, y_true)
print(f'MSE Loss: {loss.item()}')
1.3 SmoothL1Loss
SmoothL1Loss可以做到在损失较小时表现为 L2 损失,而在损失较大时表现为 L1 损失。
SmoothL1Loss 的公式如下:
SmoothL1Loss
(
x
)
=
{
0.5
⋅
x
2
,
if
∣
x
∣
<
1
∣
x
∣
−
0.5
,
otherwise
\text{SmoothL1Loss}(x) = \begin{cases} 0.5 \cdot x^2, & \text{if } |x| < 1 \\ |x| - 0.5, & \text{otherwise} \end{cases}
SmoothL1Loss(x)={0.5⋅x2,∣x∣−0.5,if ∣x∣<1otherwise
其中,
x
x
x 表示预测值和真实值之间的误差,即
x
=
y
i
−
y
^
i
x = y_i - \hat{y}_i
x=yi−y^i。
所有样本的平均损失为:
L
=
1
n
∑
i
=
1
n
L
i
\\L=\frac{1}{n} \sum_{i=1}^{n} L_{i}
L=n1i=1∑nLi
特点:
-
平滑过渡:当误差较小时,损失函数表现为 L2 Loss(平方惩罚);当误差较大时,损失函数逐渐向 L1 Loss过渡。这种平滑过渡既能对大误差有所控制,又不会对异常值过度敏感。
-
稳健性:对于异常值更加稳健,同时在小误差范围内提供了较好的优化效果。
应用场景:
SmoothL1Loss常用于需要对大误差进行一定控制但又不希望完全忽略小误差的回归任务。特别适用于目标检测任务中的边界框回归,如 Faster R-CNN 等算法中。
示例:
y
^
=
[
1.0
,
2.0
,
3.0
,
4.0
]
y
=
[
3.0
,
2.5
,
3.5
,
4.5
]
x
1
=
3
−
1
=
2
x
2
=
2.5
−
2.0
=
0.5
x
3
=
3.5
−
3.0
=
0.5
x
4
=
4.5
−
4.0
=
0.5
L
1
=
2
−
0.5
=
1.5
L
2
=
L
3
=
L
4
=
0.5
×
0.
5
2
=
0.125
L
=
1
4
(
L
1
+
L
2
+
L
3
+
L
4
)
=
1
4
(
1.5
+
0.125
×
3
)
=
0.46875
\begin{aligned} \hat{y} & =[1.0,2.0,3.0,4.0] \\ y & =[3.0,2.5,3.5,4.5] \\ x_{1} & =3-1=2 \\ x_{2} &=2.5-2.0=0.5\\ x_{3} &=3.5-3.0=0.5 \\ x_{4} & =4.5-4.0=0.5 \\ L_{1} & =2-0.5=1.5\\ L_{2} & =L_{3}=L_{4}=0.5 \times 0.5^2=0.125 \\ L & =\frac{1}{4}\left(L_{1}+L_{2}+L_{3}+L_{4}\right) \\ & =\frac{1}{4}(1.5+0.125 \times 3) \\ & =0.46875 \end{aligned}
y^yx1x2x3x4L1L2L=[1.0,2.0,3.0,4.0]=[3.0,2.5,3.5,4.5]=3−1=2=2.5−2.0=0.5=3.5−3.0=0.5=4.5−4.0=0.5=2−0.5=1.5=L3=L4=0.5×0.52=0.125=41(L1+L2+L3+L4)=41(1.5+0.125×3)=0.46875
代码实现参考如下:
import torch
import torch.nn as nn
# 创建模型的预测值和真实值
predictions = torch.tensor([1.0, 2.0, 3.0, 4.0])
targets = torch.tensor([3.0, 2.5, 3.5, 4.5])
# 计算损失方式1
smooth_loss = nn.SmoothL1Loss()
loss1 = smooth_loss(predictions, targets) # 0.46875
# 计算损失方式2
loss2 = nn.functional.smooth_l1_loss(predictions, targets)
print("模型预测值:", predictions)
print("真实值:", targets)
print("均方误差损失:", loss1, loss2)
2. CrossEntropyLoss
交叉熵损失函数,使用在输出层使用softmax激活函数进行多分类时,一般都采用交叉熵损失函数。
对于多分类问题,CrossEntropyLoss 公式如下:
CrossEntropyLoss
(
y
,
y
^
)
=
−
∑
i
=
1
C
y
i
log
(
y
^
i
)
\text{CrossEntropyLoss}(y, \hat{y}) = - \sum_{i=1}^{C} y_i \log(\hat{y}_i)
CrossEntropyLoss(y,y^)=−i=1∑Cyilog(y^i)
其中:
- C C C 是类别的总数。
- y y y 是真实标签的one-hot编码向量,表示真实类别。
- y ^ \hat{y} y^ 是模型的输出(经过 softmax 后的概率分布)。
- y i y_i yi 是真实类别的第 i i i 个元素(0 或 1)。
- y ^ i \hat{y}_i y^i 是预测的类别概率分布中对应类别 i i i 的概率。
特点:
Softmax 直白来说就是将网络输出的 logits 通过 softmax 函数,就映射成为(0,1)的值,而这些值的累和为1(满足概率的性质),那么我们将它理解成概率,选取概率最大(也就是值对应最大的)节点,作为我们的预测目标类别。
示例代码如下:
import torch
import torch.nn as nn
# 假设有三个类别,模型输出是未经softmax的logits
logits = torch.tensor([[1.5, 2.0, 0.5], [0.5, 1.0, 1.5]])
# 真实的标签
labels = torch.tensor([1, 2]) # 第一个样本的真实类别为1,第二个样本的真实类别为2
# 初始化CrossEntropyLoss
criterion = nn.CrossEntropyLoss()
# 计算损失
loss = criterion(logits, labels)
print(f'Cross Entropy Loss: {loss.item()}')
3. BCELoss
二分类交叉熵损失函数,使用在输出层使用sigmoid激活函数进行二分类时。
对于二分类问题,CrossEntropyLoss 的简化版本称为二元交叉熵(Binary Cross-Entropy Loss),公式为:
BinaryCrossEntropy
(
y
,
y
^
)
=
−
[
y
log
(
y
^
)
+
(
1
−
y
)
log
(
1
−
y
^
)
]
\text{BinaryCrossEntropy}(y, \hat{y}) = - \left[ y \log(\hat{y}) + (1 - y) \log(1 - \hat{y}) \right]
BinaryCrossEntropy(y,y^)=−[ylog(y^)+(1−y)log(1−y^)]
log的底数一般默认为e,y是真实类别目标,根据公式可知L是一个分段函数 :
L
=
−
l
o
g
(
s
i
g
m
o
i
d
激活值
)
,
当
y
=
1
L
=
−
l
o
g
(
1
−
s
i
g
m
o
i
d
激活值
)
,
当
y
=
0
L = -log(sigmoid激活值), 当y=1 \\ L = -log(1-sigmoid激活值), 当y=0
L=−log(sigmoid激活值),当y=1L=−log(1−sigmoid激活值),当y=0
以上损失函数是一个样本的损失值,总样本的损失值是求损失均值即可。
示例:
import torch
import torch.nn as nn
# y 是模型的输出,已经被sigmoid处理过,确保其值域在(0,1)
y = torch.tensor([[0.7], [0.2], [0.9], [0.7]])
# targets 是真实的标签,0或1
t = torch.tensor([[1], [0], [1], [0]], dtype=torch.float)
# 计算损失方式一:
bceLoss = nn.BCELoss()
loss1 = bceLoss(y, t)
#计算损失方式二: 两种方式结果相同
loss2 = nn.functional.binary_cross_entropy(y, t)
print(loss1, loss2)
4. 总结
-
当输出层使用softmax多分类时,使用交叉熵损失函数;
-
当输出层使用sigmoid二分类时,使用二分类交叉熵损失函数, 比如在逻辑回归中使用;
-
当功能为线性回归时,使用smooth L1损失函数或均方差损失-L2 loss;