神经网络基础
1 神经网络
深度学习神经网络就是大脑仿生,数据从输入到输出经过一层一层的神经元产生预测值的过程就是前向传播(也叫正向传播)。
前向传播涉及到人工神经元是如何工作的(也就是神经元的初始化、激活函数),神经网络如何搭建,权重参数计算、数据形如何状变化。千里之行始于足下,我们一起进入深度学习的知识海洋吧。
1.1 神经网络概念
1.1.1 什么是神经网络
人工神经网络(Artificial Neural Network, 简写为ANN )也简称为神经网络(NN),是一种模仿生物神经网络结构和功能的计算模型。它由多个互相连接的人工神经元(也称为节点)构成,可以用于处理和学习复杂的数据模式,尤其适合解决非线性问题。人工神经网络是机器学习中的一个重要模型,尤其在深度学习领域中得到了广泛应用。
人脑可以看做是一个生物神经网络,由众多的神经元 连接而成。各个神经元传递复杂的电信号,树突接收到输入信号 ,然后对信号进行处理,通过轴突输出信号。下图是生物神经元示意图:
当电信号通过树突进入到细胞核时,会逐渐聚集电荷。达到一定的电位后,细胞就会被激活,通过轴突发出电信号。
1.1.2 如何构建神经网络
神经网络是由多个神经元组成,构建神经网络就是在构建神经元。以下是神经网络中神经元的构建说明:
这个流程就像,来源不同树突(树突都会有不同的权重)的信息, 进行的加权计算, 输入到细胞中做加和,再通过激活函数输出细胞值。
同一层的多个神经元可以看作是通过并行计算来处理相同的输入数据,学习输入数据的不同特征。每个神经元可能会关注输入数据中的不同部分,从而捕捉到数据的不同属性。
接下来,我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个强度,如下图所示:
神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动。其中的基本部分是:
- 输入层(Input Layer): 即输入x的那一层(如图像、文本、声音等)。每个输入特征对应一个神经元。输入层将数据传递给下一层的神经元。
- 输出层(Output Layer): 即输出y的那一层。输出层的神经元根据网络的任务(回归、分类等)生成最终的预测结果。
- 隐藏层(Hidden Layers): 输入层和输出层之间都是隐藏层,神经网络的"深度"通常由隐藏层的数量决定。隐藏层的神经元通过加权和激活函数处理输入,并将结果传递到下一层。
特点是:
- 同一层的神经元之间没有连接
- 第N层的每个神经元和第N-1层的所有神经元相连(这就是Fully Connected的含义),这就是全连接神经网络(FCNN)
- 全连接神经网络接收的样本数据是二维的,数据在每一层之间需要以二维的形式传递
- 第N-1层神经元的输出就是第N层神经元的输入
- 每个连接都有一个权重值(w系数和b系数)
1.1.3 神经网络内部状态值和激活值
每一个神经元工作时,前向传播 会产生两个值,内部状态值(加权求和值)和激活值 ;反向传播 时会产生激活值梯度 和内部状态值梯度。
-
内部状态值
- 神经元或隐藏单元的内部存储值,它反映了当前神经元接收到的输入、历史信息以及网络内部的权重计算结果。
- 每个输入 x i x_i xi都有一个与之相乘的权重 w i w_i wi,表示每个输入信号的重要性。
- z=w⋅x+b
- w:权重矩阵
- x:输入值
- b:偏置
-
激活值
- 通过激活函数(如 ReLU、Sigmoid、Tanh)对内部状态值进行非线性变换后得到的结果。激活值决定了当前神经元的输出。
- a=f(z)
- f:激活函数
- z:内部状态值
通过控制每个神经元的内部状态值、激活值的大小;每一层的内部状态值的方差、每一层的激活值的方差可让整个神经网络工作的更好。
所以下面两个小结,我们将要学习神经元的激活函数,神经元的权重初始化。
1.2 激活函数
1.2.1 网络非线性因素理解
没有引入非线性因素的网络等价于使用一个线性模型来拟合
通过给网络输出增加激活函数, 实现引入非线性因素, 使得网络模型可以逼近任意函数, 提升网络对复杂问题的拟合能力
激活函数 用于对每层的输出数据进行变换 , 进而为整个网络注入了非线性因素 。此时, 神经网络就可以拟合各种曲线。如果不使用激活函数,整个网络虽然看起来复杂,其本质还相当于一种线性模型,如下公式所示:
另外通过图像可视化的形式理解:
我们发现增加激活函数之后, 对于线性不可分的场景,神经网络的拟合能力更强。
1.2.2 常见激活函数
激活函数主要用来向神经网络中加入非线性因素,以解决线性模型表达能力不足的问题,它对神经网络有着极其重要的作用。我们的网络参数在更新时,使用的反向传播算法(BP),这就要求我们的激活函数必须可微。
1.2.2.1 Sigmoid 激活函数
激活函数公式:
激活函数求导公式:
sigmoid 激活函数的函数图像如下:
-
从sigmoid函数图像可以得到,sigmoid 函数可以将任意的输入 映射到 (0, 1) 之间,当输入的值大致在**<-6或者>6**时,意味着输入任何值得到的激活值都是差不多的,这样会丢失部分的信息。比如:输入100和输入10000经过 sigmoid的激活值几乎都是等于1的,但是输入的数据之间相差100倍的信息就丢失了。
-
对于sigmoid函数而言,输入值在**[-6, 6]之间输出值才会 有明显差异**,输入值在**[-3, 3]之间才会有比较好的效果**
-
通过上述导数图像,我们发现导数数值范围是 (0, 0.25) ,当输入的值**<-6或者>6时,sigmoid激活函数图像的 导数接近为 0**,此时网络参数将更新极其缓慢,或者无法更新。
-
一般来说,sigmoid网络在5层之内 就会产生梯度消失 现象。而且,该激活函数的激活值并不是以0为中心的,激活值总是偏向正数,导致梯度更新时,只会对某些特征产生相同方向的影响,所以在实践中这种激活函数使用的很少。sigmoid函数一般只用于二分类的输出层。
在 PyTorch中使用sigmoid函数的示例代码如下:
python
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
"""
绘制激活函数图像时出现以下提示,需要将anaconda3/Lib/site-packages/torch/lib目录下的libiomp5md.dll文件删除
OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
"""
# 创建画布和坐标轴
_, axes = plt.subplots(1, 2)
# 函数图像
x = torch.linspace(-20, 20, 1000)
# 输入值x通过sigmoid函数转换成激活值y
y = torch.sigmoid(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Sigmoid 函数图像')
# 导数图像
x = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.sigmoid(x).sum().backward()
# x.detach():输入值x的ndarray数组
# x.grad:计算梯度,求导
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Sigmoid 导数图像')
plt.show()
1.2.2.2 Tanh 激活函数
Tanh叫做双曲正切函数,其公式如下:
激活函数求导公式:
Tanh的函数图像、导数图像如下:
-
由上面的函数图像可以看到,Tanh函数将输入映射到(-1, 1)之间 ,图像以0为中心,激活值在0点对称,当输入的值大概**<-3或者>3** 时将被映射为-1或者1。其导数值范围 (0, 1),当输入的值大概**<-3或者>3**时,其导数近似0。
-
与Sigmoid相比,它是以0为中心的,使得其收敛速度要比Sigmoid快,减少迭代次数。然而,从图中可以看出,Tanh两侧的导数也为0,同样会造成梯度消失。
-
若使用时可在隐藏层使用tanh函数 ,在输出层使用sigmoid函数。
在 PyTorch 中使用tanh函数的示例代码如下:
python
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
_, axes = plt.subplots(1, 2)
# 函数图像
x = torch.linspace(-20, 20, 1000)
y = torch.tanh(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Tanh 函数图像')
# 导数图像
x = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.tanh(x).sum().backward()
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Tanh 导数图像')
plt.show()
1.2.2.3 ReLU 激活函数
ReLU 激活函数公式如下:
激活函数求导公式:
ReLU 的函数图像、导数图像如下:
- ReLU 激活函数将小于0的值映射为0,而大于0的值则保持不变,它更加重视正信号,而忽略负信号,这种激活函数运算更为简单,能够提高模型的训练效率。
- 当x<0时,ReLU导数为0,而当x>0时,则不存在饱和问题。所以,ReLU 能够在x>0时保持梯度不衰减,从而缓解梯度消失问题。然而,随着训练的推进,部分输入会落入小于0区域,导致对应权重无法更新。这种现象被称为"神经元死亡"。
- ReLU是目前最常用的激活函数。与sigmoid相比,RELU的优势是:
- 采用sigmoid函数,计算量大(指数运算),反向传播求误差梯度时,计算量相对大;而采用Relu激活函数,整个过程的计算量节省很多
- sigmoid函数反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练;而采用relu激活函数,当输入的值>0时,梯度为1,不会出现梯度消失的情况
- Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生
在 PyTorch 中使用ReLU函数的示例代码如下:
python
import torch
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
_, axes = plt.subplots(1, 2)
# 函数图像
x = torch.linspace(-20, 20, 1000)
y = torch.relu(x)
axes[0].plot(x, y)
axes[0].grid()
axes[0].set_title('Tanh 函数图像')
# 导数图像
x = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.relu(x).sum().backward()
axes[1].plot(x.detach(), x.grad)
axes[1].grid()
axes[1].set_title('Tanh 导数图像')
plt.show()
1.2.2.4 SoftMax激活函数
softmax用于多分类 过程中,它是二分类函数sigmoid在多分类上的推广,目的是将多分类的结果以概率的形式展现出来。
计算方法如下图所示:
SoftMax就是将网络输出的logits通过softmax函数,映射成为(0,1)的值,而这些值的累和为1(满足概率的性质),那么我们将它理解成概率,选取概率最大(也就是值对应最大的)节点,作为我们的预测目标类别。
在 PyTorch 中使用SoftMax函数的示例代码如下:
python
import torch
scores = torch.tensor([0.2, 0.02, 0.15, 0.15, 1.3, 0.5, 0.06, 1.1, 0.05, 3.75])
# dim=0, 按行计算
probabilities = torch.softmax(scores, dim=0)
print(probabilities)
程序输出结果:
python
tensor([0.0212, 0.0177, 0.0202, 0.0202, 0.0638, 0.0287, 0.0185, 0.0522, 0.0183,
0.7392])
1.2.3 如何选择激活函数
除了上述的激活函数,还存在很多其他的激活函数,如下图所示:
对于隐藏层:
- 优先选择ReLU激活函数
- 如果ReLu效果不好,那么尝试其他激活,如Leaky ReLu等。
- 如果你使用了ReLU, 需要注意一下Dead ReLU问题,避免出现0梯度从而导致过多的神经元死亡。
- 少使用sigmoid激活函数,可以尝试使用tanh激活函数
对于输出层:
- 二分类问题选择sigmoid激活函数
- 多分类问题选择softmax激活函数
- 回归问题选择identity激活函数
1.3 参数初始化
我们在构建网络之后,网络中的参数是需要初始化的。我们需要初始化的参数主要有权重 和偏置 ,偏置一般初始化为0即可,而对权重的初始化则会更加重要。
参数初始化的作用:
- 防止梯度消失或爆炸:初始权重值过大或过小会导致梯度在反向传播中指数级增大或缩小。
- 提高收敛速度:合理的初始化使得网络的激活值分布适中,有助于梯度高效更新。
- 保持对称性破除:权重的初始化需要打破对称性,否则网络的学习能力会受到限制。
1.3.1 常见参数初始化方法
-
随机初始化
-
均匀分布初始化:权重参数初始化从区间均匀随机取值,默认区间为(0,1)。可以设置为在(- 1 d 1\over\sqrt{d} d 1, 1 d 1\over\sqrt{d} d 1)均匀分布中生成当前神经元的权重,其中d为神经元的输入数量。
-
正态分布初始化:随机初始化从均值为0,标准差是1的高斯分布中取样,使用一些很小的值对参数W进行初始化
-
优点:能有效打破对称性
-
缺点:随机选择范围不当可能导致梯度问题
-
适用场景:浅层网络或低复杂度模型。隐藏层1-3层,总层数不超过5层。
-
-
全0初始化:将神经网络中的所有权重参数初始化为0
- 优点:实现简单
- 缺点:无法打破对称性,所有神经元更新方向相同,无法有效训练
- 适用场景:几乎不使用,仅用于偏置项的初始化
-
全1初始化:将神经网络中的所有权重参数初始化为1
- 优点:实现简单
- 缺点
- 无法打破对称性,所有神经元更新方向相同,无法有效训练
- 会导致激活值在网络中呈指数增长,容易出现梯度爆炸
- 适用场景
- 测试或调试:比如验证神经网络是否能正常前向传播和反向传播
- 特殊模型结构:某些稀疏网络或特定的自定义网络中可能需要手动设置部分参数为1
- 偏置初始化:偶尔可以将偏置初始化为小的正值(如 0.1),但很少用1作为偏置的初始值
-
固定值初始化:将神经网络中的所有权重参数初始化为某个固定值
- 优点:实现简单
- 缺点
- 无法打破对称性,所有神经元更新方向相同,无法有效训练
- 初始权重过大或过小可能导致梯度爆炸或梯度消失
- 适用场景
- 测试或调试
-
kaiming初始化 ,也叫做HE初始化:专为ReLU和其变体设计,考虑到ReLU激活函数的特性,对输入维度进行缩放
- HE初始化分为正态分布的HE初始化、均匀分布的HE初始化
- 正态分布的he初始化
- w权重值从均值为0, 标准差为std中随机采样,std =
sqrt(2 / fan_in)
- std值越大,w权重值离均值0分布相对较广,计算得到的内部状态值有较大的正值或负值
- w权重值从均值为0, 标准差为std中随机采样,std =
- 均匀分布的he初始化
- 它从[-limit,limit] 中的均匀分布中抽取样本,
limit
是sqrt(6 / fan_in)
- 它从[-limit,limit] 中的均匀分布中抽取样本,
fan_in
输入神经元的个数,当前层 接受的来自上一层的神经元的数量。简单来说,就是当前层接收多少个输入
- 正态分布的he初始化
- 优点:适合 ReLU,能保持梯度稳定
- 缺点:对非 ReLU 激活函数效果一般
- 适用场景:深度网络(10层及以上),使用 ReLU、Leaky ReLU 激活函数
- HE初始化分为正态分布的HE初始化、均匀分布的HE初始化
-
xavier初始化 ,也叫做Glorot初始化:根据网络输入和输出的维度自动选择权重范围,使输入和输出的方差相同
-
xavier初始化分为正态分布的xavier初始化、均匀分布的xavier初始化
- 正态化的Xavier初始化
- w权重值从均值为0, 标准差为std中随机采样,std =
sqrt(2 / (fan_in + fan_out))
- std值越小,w权重值离均值0分布相对集中,计算得到的内部状态值有较小的正值或负值
- w权重值从均值为0, 标准差为std中随机采样,std =
- 均匀分布的Xavier初始化
- [-limit,limit] 中的均匀分布中抽取样本, limit 是
sqrt(6 / (fan_in + fan_out))
- [-limit,limit] 中的均匀分布中抽取样本, limit 是
- fan_in 是输入神经元个数,当前层 接受的来自上一层的神经元的数量。简单来说,就是当前层接收多少个输入
- fan_out 是输出神经元个数,当前层 输出的神经元的数量,也就是当前层会传递给下一层的神经元的数量。简单来说,就是当前层会产生多少个输出。
- 正态化的Xavier初始化
-
优点:适用于Sigmoid、Tanh 等激活函数,解决梯度消失问题
-
缺点:对 ReLU 等激活函数表现欠佳
-
适用场景:深度网络(10层及以上),使用 Sigmoid 或 Tanh 激活函数
-
python
import torch.nn as nn
# 1. 均匀分布随机初始化
def test01():
linear = nn.Linear(5, 3)
# 从0-1均匀分布产生参数
nn.init.uniform_(linear.weight)
nn.init.uniform_(linear.bias)
print(linear.weight.data)
# 2. 固定初始化
def test02():
linear = nn.Linear(5, 3)
nn.init.constant_(linear.weight, 5)
print(linear.weight.data)
# 3. 全0初始化
def test03():
linear = nn.Linear(5, 3)
nn.init.zeros_(linear.weight)
print(linear.weight.data)
# 4. 全1初始化
def test04():
linear = nn.Linear(5, 3)
nn.init.ones_(linear.weight)
print(linear.weight.data)
# 5. 正态分布随机初始化
def test05():
linear = nn.Linear(5, 3)
nn.init.normal_(linear.weight, mean=0, std=1)
print(linear.weight.data)
# 6. kaiming 初始化
def test06():
# kaiming 正态分布初始化
linear = nn.Linear(5, 3)
nn.init.kaiming_normal_(linear.weight)
print(linear.weight.data)
# kaiming 均匀分布初始化
linear = nn.Linear(5, 3)
nn.init.kaiming_uniform_(linear.weight)
print(linear.weight.data)
# 7. xavier 初始化
def test07():
# xavier 正态分布初始化
linear = nn.Linear(5, 3)
nn.init.xavier_normal_(linear.weight)
print(linear.weight.data)
# xavier 均匀分布初始化
linear = nn.Linear(5, 3)
nn.init.xavier_uniform_(linear.weight)
print(linear.weight.data)
1.3.2 如何选择参数初始化
-
激活函数的选择:根据激活函数的类型选择对应的初始化方法
- Sigmoid/Tanh:xavier 初始化
- ReLU/Leaky ReLU:kaiming 初始化
-
神经网络模型的深度
- 浅层网络:随机初始化即可
- 深层网络:需要考虑方差平衡,如 xavier 或 kaiming 初始化
1.4 神经网络搭建和参数计算
1.4.1 构建神经网络
在pytorch中定义深度神经网络其实就是层堆叠的过程,继承自nn.Module,实现两个方法:
__init__
方法中定义网络中的层结构,主要是全连接层,并进行初始化- forward方法,在实例化模型的时候,底层会自动调用该函数。该函数中为初始化定义的layer传入数据,进行前向传播等。
接下来我们来构建如下图所示的神经网络模型:
编码设计如下:
- 第1个隐藏层:权重初始化采用标准化的xavier初始化 激活函数使用sigmoid
- 第2个隐藏层:权重初始化采用标准化的He初始化 激活函数采用relu
- out输出层线性层 假若多分类,采用softmax做数据归一化
构造神经网络模型代码:
python
import torch
import torch.nn as nn
from torchsummary import summary # 计算模型参数,查看模型结构, pip install torchsummary -i https://mirrors.aliyun.com/pypi/simple/
# 创建神经网络模型类
class Model(nn.Module):
# 初始化属性值
def __init__(self):
# 调用父类的初始化属性值
super(Model, self).__init__()
# 创建第一个隐藏层模型, 3个输入特征,3个输出特征
self.linear1 = nn.Linear(3, 3)
# 初始化权重
nn.init.xavier_normal_(self.linear1.weight)
nn.init.zeros_(self.linear1.bias)
# 创建第二个隐藏层模型, 3个输入特征(上一层的输出特征),2个输出特征
self.linear2 = nn.Linear(3, 2)
# 初始化权重
nn.init.kaiming_normal_(self.linear2.weight, nonlinearity='relu')
nn.init.zeros_(self.linear2.bias)
# 创建输出层模型
self.out = nn.Linear(2, 2)
# 创建前向传播方法,自动执行forward()方法
def forward(self, x):
# 数据经过第一个线性层
x = self.linear1(x)
# 使用sigmoid激活函数
x = torch.sigmoid(x)
# 数据经过第二个线性层
x = self.linear2(x)
# 使用relu激活函数
x = torch.relu(x)
# 数据经过输出层
x = self.out(x)
# 使用softmax激活函数
# dim=-1:每一维度行数据相加为1
x = torch.softmax(x, dim=-1)
return x
训练神经网络模型代码:
python
# 创建构造模型函数
def train():
# 实例化model对象
my_model = Model()
# 随机产生数据
my_data = torch.randn(5, 3)
print("my_data-->", my_data)
print("my_data shape", my_data.shape)
# 数据经过神经网络模型训练
output = my_model(my_data)
print("output-->", output)
print("output shape-->", output.shape)
# 计算模型参数
# 计算每层每个神经元的w和b个数总和
print("======计算模型参数======")
summary(my_model, input_size=(3,), batch_size=5)
# 查看模型参数
print("======查看模型参数w和b======")
for name, parameter in my_model.named_parameters():
print(name, parameter)
if __name__ == '__main__':
train()
1.4.2 观察数据形状变化
-
观察程序输入和输出的数据形状变化
- 输入5行数据,输出也是5行数据
- 输入5行数据3个特征,经过第一个隐藏层是3个特征,经过第二个隐藏层是2个特征,经过输出层是2个特征
- 模型最终预测结果是:5行2列数据
pythonmydata.shape---> torch.Size([5, 3]) output.shape---> torch.Size([5, 2]) mydata---> tensor([[-0.3714, -0.8578, -1.6988], [ 0.3149, 0.0142, -1.0432], [ 0.5374, -0.1479, -2.0006], [ 0.4327, -0.3214, 1.0928], [ 2.2156, -1.1640, 1.0289]]) output---> tensor([[0.5095, 0.4905], [0.5218, 0.4782], [0.5419, 0.4581], [0.5163, 0.4837], [0.6030, 0.3970]], grad_fn=<SoftmaxBackward>)
1.4.3 模型参数计算
-
模型参数的计算
-
以第一个隐层为例:该隐层有3个神经元,每个神经元的参数为:4个(w1,w2,w3,b1),所以一共用3x4=12个参数。
-
输入数据和网络权重是两个不同的事儿!对于初学者理解这一点十分重要,要分得清。
python---------------------------------------------------------------- Layer (type) Output Shape Param # ================================================================ Linear-1 [5, 3] 12 Linear-2 [5, 2] 8 Linear-3 [5, 2] 6 ================================================================ Total params: 26 Trainable params: 26 Non-trainable params: 0 ---------------------------------------------------------------- Input size (MB): 0.00 Forward/backward pass size (MB): 0.00 Params size (MB): 0.00 Estimated Total Size (MB): 0.00 ----------------------------------------------------------------
-
1.4.4 查看模型参数
-
通常继承nn.Module,撰写自己的网络层。它强大的封装不需要我们定义可学习的参数(比如卷积核的权重和偏置参数)。
-
如何才能查看封装好的,可学习的网络参数哪?
- 模块实例名.name_parameters(),会分别返回name和parameter
python# 实例化model对象 mymodel = Model() # 查看网络参数 for name, parameter in mymodel.named_parameters(): # print('name--->', name) # print('parameter--->', parameter) print(name, parameter)
结果显示
pythonlinear1.weight Parameter containing: tensor([[ 0.1715, -0.3711, 0.1692], [-0.2497, -0.6156, -0.4235], [-0.7090, -0.0380, 0.4790]], requires_grad=True) linear1.bias Parameter containing: tensor([-0.2320, 0.3431, 0.2771], requires_grad=True) linear2.weight Parameter containing: tensor([[-0.5044, -0.7435, -0.6736], [ 0.6908, -0.1466, -0.0019]], requires_grad=True) linear2.bias Parameter containing: tensor([0.2340, 0.4730], requires_grad=True) out.weight Parameter containing: tensor([[ 0.5185, 0.4019], [-0.4313, -0.3438]], requires_grad=True) out.bias Parameter containing: tensor([ 0.4521, -0.6339], requires_grad=True)
2 损失函数
2.1 损失函数概念
在深度学习中, 损失函数是用来衡量模型参数质量的函数, 衡量的方式是比较网络输出(预测值)和真实输出(真实值)的差异。
模型通过最小化损失函数的值来调整参数,使其输出更接近真实值。
损失函数在不同的文献中名称是不一样的,主要有以下几种命名方式:
损失函数作用:
- 评估性能:反映模型预测结果与目标值的匹配程度。
- 指导优化:通过梯度下降等算法最小化损失函数,优化模型参数。
2.2 分类任务损失函数
在深度学习的分类任务 中使用最多的是交叉熵损失函数,所以在这里我们着重介绍这种损失函数。
2.2.1 多分类任务损失函数
在多分类任务通常使用softmax将logits转换为概率的形式,所以多分类的交叉熵损失也叫做softmax损失,它的计算方法是:
其中:
- yi是样本x属于某一个类别的真实概率
- 而f(x)是样本属于某一类别的预测分数
- S是softmax激活函数,将属于某一类别的预测分数转换成概率
- L用来衡量真实值y和预测值f(x)之间差异性的损失结果
例子:
上图中的交叉熵损失为:
从概率角度理解,我们的目的是最小化正确类别所对应的预测概率的对数的负值(损失值最小),如下图所示:
在PyTorch中使用nn.CrossEntropyLoss()
实现,如下所示:
python
import torch
from torch import nn
# 分类损失函数:交叉熵损失使用nn.CrossEntropyLoss()实现。nn.CrossEntropyLoss()=softmax + 损失计算
def test01():
# 设置真实值: 可以是热编码后的结果也可以不进行热编码
# y_true = torch.tensor([[0, 1, 0], [0, 0, 1]], dtype=torch.float32)
# 注意的类型必须是64位整型数据
y_true = torch.tensor([1, 2], dtype=torch.int64)
y_pred = torch.tensor([[0.2, 0.6, 0.2], [0.1, 0.8, 0.1]], requires_grad=True, dtype=torch.float32)
# 实例化交叉熵损失,默认求平均损失
# reduction='sum':总损失
loss = nn.CrossEntropyLoss()
# 计算损失结果
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)
2.2.2 二分类任务损失函数
在处理二分类任务时,我们不再使用softmax激活函数,而是使用sigmoid激活函数,那损失函数也相应的进行调整,使用二分类的交叉熵损失函数:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
其中:
-
y是样本x属于某一个类别的真实概率
-
而y^是样本属于某一类别的预测概率
-
L用来衡量真实值y与预测值y^之间差异性的损失结果。
在PyTorch中实现时使用nn.BCELoss()
实现,如下所示:
python
import torch
from torch import nn
def test02():
# 1 设置真实值和预测值
# 预测值是sigmoid输出的结果
y_pred = torch.tensor([0.6901, 0.5459, 0.2469], requires_grad=True)
y_true = torch.tensor([0, 1, 0], dtype=torch.float32)
# 2 实例化二分类交叉熵损失
loss = nn.BCELoss()
# 3 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)
2.3 回归任务损失函数
2.3.1 MAE损失函数
**Mean absolute loss(MAE)**也被称为L1 Loss,是以绝对误差作为距离
损失函数公式:
曲线如下图所示:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
特点是:
- 由于L1 loss具有稀疏性,为了惩罚较大的值,因此常常将其作为正则项添加到其他loss中作为约束。((0点不可导, 产生稀疏矩阵))
- L1 loss的最大问题是梯度在零点不平滑,导致会跳过极小值
- 适用于回归问题中存在异常值或噪声数据时,可以减少对离群点的敏感性
在PyTorch中使用nn.L1Loss()
实现,如下所示:
python
import torch
from torch import nn
# 计算inputs与target之差的绝对值
def test03():
# 1 设置真实值和预测值
y_pred = torch.tensor([1.0, 1.0, 1.9], requires_grad=True)
y_true = torch.tensor([2.0, 2.0, 2.0], dtype=torch.float32)
# 2 实例MAE损失对象
loss = nn.L1Loss()
# 3 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)
2.3.2 MSE损失函数
**Mean Squared Loss/ Quadratic Loss(MSE loss)**也被称为L2 loss,或欧氏距离,它以误差的平方和的均值作为距离
损失函数公式:
曲线如下图所示:
特点是:
-
L2 loss也常常作为正则项,对于离群点(outliers)敏感,因为平方项会放大大误差
-
当预测值与目标值相差很大时, 梯度容易爆炸
- 梯度爆炸:网络层之间的梯度(值大于 1.0)重复相乘导致的指数级增长会产生梯度爆炸
-
适用于大多数标准回归问题,如房价预测、温度预测等
在PyTorch中通过nn.MSELoss()
实现:
python
import torch
from torch import nn
def test04():
# 1 设置真实值和预测值
y_pred = torch.tensor([1.0, 1.0, 1.9], requires_grad=True)
y_true = torch.tensor([2.0, 2.0, 2.0], dtype=torch.float32)
# 2 实例MSE损失对象
loss = nn.MSELoss()
# 3 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('myloss:', my_loss)
2.3.3 Smooth L1损失函数
smooth L1说的是光滑之后的L1,是一种结合了均方误差(MSE)和平均绝对误差(MAE)优点的损失函数。它在误差较小时表现得像 MSE,在误差较大时则更像 MAE。
Smooth L1损失函数如下式所示:
其中:𝑥=f(x)−y
为真实值和预测值的差值。
从上图中可以看出,该函数实际上就是一个分段函数
- 在[-1,1]之间实际上就是L2损失,这样解决了L1的不光滑问题
- 在[-1,1]区间外,实际上就是L1损失,这样就解决了离群点梯度爆炸的问题
特点是:
-
对离群点更加鲁棒:当误差较大时,损失函数会线性增加(而不是像MSE那样平方增加),因此它对离群点的惩罚更小,避免了MSE对离群点过度敏感的问题
-
计算梯度时更加平滑:与MAE相比,Smooth L1在小误差时表现得像MSE,避免了在训练过程中因使用绝对误差而导致的梯度不连续问题
在PyTorch中使用nn.SmoothL1Loss()
计算该损失,如下所示:
python
import torch
from torch import nn
def test05():
# 1 设置真实值和预测值
y_true = torch.tensor([0, 3])
y_pred = torch.tensor([0.6, 0.4], requires_grad=True)
# 2 实例smmothL1损失对象
loss = nn.SmoothL1Loss()
# 3 计算损失
my_loss = loss(y_pred, y_true).detach().numpy()
print('loss:', my_loss)