【深度学习】神经网络基础

【深度学习】神经网络基础

1、神经网络

1.1、神经元

人工神经网络( Artificial Neural Network, 简写为ANN)也简称为神经网络(NN),是一种模仿生物神经网络结构和功能的计算模型,由一个个神经元构成。人脑可以看做是一个生物神经网络,由众多的神经元连接而成。各个神经元传递复杂的电信号,树突接收到输入信号,然后对信号进行处理,通过轴突输出信号。下图是生物神经元示意图:

当电信号通过树突进入到细胞核时,会逐渐聚集电荷。达到一定的电位后,细胞就会被激活,通过轴突发出电信号。

那怎么构建人工神经网络中的神经元呢?

我们假如输入的是一张图片,一张图片上有很多个像素点,w1、w2、w3...,我们将其做一个多元线性回归进行加权和 w1x1 + w2x2 + w3x3 + ... + b,再加一个激活函数,激活函数来控制神经元的刺激是否需要向后传导,如果刺激过小就不需要,如果刺激过大就需要向后传导。这个过程就像,来源不同树突(树突都会有不同的权重)的信息, 进行的加权计算, 输入到细胞中做加和,再通过激活函数输出细胞值。

  • 深度学习是有监督学习!
  • 神经元 = 加权和 + 激活函数

1.2、构建神经网络

接下来,我们使用多个神经元来构建神经网络,相邻层之间的神经元相互连接,并给每一个连接分配一个强度,如下图所示:

  • 输入层:输入数据
  • 输出层:输出目标(有的目标需要做加权和+激活)
  • 隐藏层:加权和+激活

神经网络特点:

  • 同一层的神经元之间没有连接
  • 第 N 层的每个神经元和第 N-1层 的所有神经元相连(这就是full connected的含义),这就是全连接神经网络
    • 前一层的每个神经元和后一层的每个神经元都有连接
  • 第 N-1 层神经元的输出就是第N层神经元的输入
  • 每个连接都有一个权重值(w系数和b系数)

其中隐藏层的层数、每个隐藏层的神经元个数都是超参数。

  • 参数是模型中可被学习和调整的参数,通过训练数据进行学习和优化。而超参数则是手动设置的参数,用于控制模型的行为和性能。
  • 参数通常是指神经网络中的权重w和偏差b

1.3、激活函数

激活函数 用于对每层的输出数据进行变换 , 进而为整个网络注入了非线性因素。此时, 神经网络就可以拟合各种曲线。

激活函数是用来加入非线性因素的,提高神经网络对模型的表达能力,解决线性模型所不能解决的问题

  1. 非线性函数有指数函数、三角函数、对数函数、分段函数等。
  2. 没有引入非线性因素的网络等价于使用一个线性模型来拟合
  3. 通过给网络输出增加激活函数, 实现引入非线性因素, 使得网络模型可以逼近任意函数, 提升网络对复杂问题的拟合能力
  • 更改超参数的值:
    • 加减号可以增加或者减少隐藏层的层数、增加或减少神经元的个数。

1.3.1、sigmoid激活函数

激活函数公式:(x是加权和,每个神经元的加权和都会送至sigmoid函数,得到每个神经元对应的值)
f ( x ) = 1 1 + e − x f(x)=\frac{1}{1+e^{-x}} f(x)=1+e−x1

激活函数求导公式:
f ′ ( x ) = 1 1 + e − x ( 1 − 1 1 + e − x ) = f ( x ) ( 1 − f ( x ) ) f'(x) =\frac{1}{1+e^{-x}}(1-\frac{1}{1+e^{-x}}) = f(x)(1-f(x)) f′(x)=1+e−x1(1−1+e−x1)=f(x)(1−f(x))

sigmoid激活函数图像如下:

  • sigmoid 函数可以将任意的输入映射到 (0, 1) 之间,当输入的值大致在 <-6 或者 >6 时,梯度是接近于0的,意味着输入任何值得到的激活值都是差不多的,这样会丢失部分的信息。比如:输入 100 和输出 10000 经过 sigmoid 的激活值几乎都是等于 1 的,但是输入的数据之间相差 100 倍的信息就丢失了
  • 对于 sigmoid 函数而言,输入值在 [-6, 6] 之间输出值才会有明显差异,输入值在 [-3, 3] 之间才会有比较好的效果。
  • 通过上述导数图像,我们发现导数数值范围是 (0, 0.25),当输入 <-6 或者 >6 时,sigmoid 激活函数图像的导数接近为 0,此时网络参数将更新极其缓慢,或者无法更新。
  • 一般来说, sigmoid 网络在 5 层之内就会产生梯度消失 现象。而且,该激活函数并不是以 0 为中心的,所以在实践中这种激活函数使用的很少。sigmoid函数一般只用于二分类(是和否)的输出层,因为最终得到的值是(0,1)之间,可以看作是概率。
python 复制代码
import torch
import matplotlib.pyplot as plt

# 生成一个包含 1000 个元素的一维张量 x,这些元素均匀分布在区间 [-20, 20] 上,用于作为函数的输入值
x = torch.linspace(-20, 20, 1000)

# 对张量 x 中的每个元素应用 Sigmoid 函数
y = torch.sigmoid(x)
plt.plot(x,y)
plt.grid()
plt.show()
python 复制代码
x = torch.linspace(-20, 20, 1000,requires_grap=True)

# 对张量 x 中的每个元素应用 Sigmoid 函数
torch.sigmoid(x).sum().backward()
plt.plot(x.detach(),x.grad)
plt.grid()
plt.show()

1.3.2、tanh 激活函数

谭忒~

激活函数公式:
f ( x ) = 1 − e − 2 x 1 + e − 2 x f(x)=\frac{1-e^{-2x}}{1+e^{-2x}} f(x)=1+e−2x1−e−2x

激活函数求导公式:
f ′ ( x ) = 1 − f 2 ( x ) f'(x) = 1-f^2(x) f′(x)=1−f2(x)

tanh激活函数图像如下:

  • Tanh 函数将输入映射到 (-1, 1) 之间,图像以 0 为中心,在 0 点对称,当输入<-3 或者 >3 时将被映射为 -1 或者 1。其导数值范围 (0, 1),当输入的值大概 <-3 或者 > 3 时,其导数近似 0
  • 与 Sigmoid 相比,它是以 0 为中心的,且梯度相对于sigmoid大,使得其收敛速度要比 Sigmoid 快,减少迭代次数。然而,从图中可以看出,Tanh 两侧的导数也为 0,同样会造成梯度消失
  • 若使用时可在隐藏层使用tanh函数 ,在输出层使用sigmoid函数
python 复制代码
import torch
import matplotlib.pyplot as plt

# 生成一个包含 1000 个元素的一维张量 x,这些元素均匀分布在区间 [-20, 20] 上,用于作为函数的输入值
x = torch.linspace(-20, 20, 1000)

# 对张量 x 中的每个元素应用 tanh 函数
y = torch.tanh(x)
plt.plot(x,y)
plt.grid()
plt.show()
python 复制代码
# 生成一个包含 1000 个元素的一维张量 x,这些元素均匀分布在区间 [-20, 20] 上,用于作为函数的输入值
x = torch.linspace(-20, 20, 1000,requires_grap=True)

# 对张量 x 中的每个元素应用 tanh 函数
 torch.tanh(x).sum.backward()
plt.plot(x.detach(),x.grad)
plt.grid()
plt.show()

1.3.3、ReLU激活函数

瑞鹿~

ReLU激活(分段函数)函数公式:
f ( x ) = m a x ( 0 , x ) f(x)=max(0,x) f(x)=max(0,x)

激活函数求导公式:
f ′ ( x ) = 0 或 1 f'(x)=0或1 f′(x)=0或1

激活函数图像如下:

  • ReLU 激活函数将小于 0 的值映射为 0,而大于 0 的值则保持不变,它更加重视正信号,而忽略负信号,这种激活函数运算更为简单,能够提高模型的训练效率。

  • 当x<0时,ReLU导数为0,而当x>0时,ReLU导数为1,则不存在饱和问题。

    • 所以,ReLU 能够在x>0时保持梯度不衰减,从而缓解梯度消失问题 。然而,随着训练的推进,部分输入会落入小于0区域,导致对应权重无法更新。这种现象被称为"神经元死亡"。
  • ReLU是目前最常用的激活函数。与sigmoid相比,RELU的优势是:

    • 采用sigmoid函数,计算量大(指数运算),反向传播求误差梯度时,计算量相对大,而采用Relu激活函数,整个过程的计算量节省很多。
    • sigmoid函数反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练。 Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生。
python 复制代码
# 生成一个包含 1000 个元素的一维张量 x,这些元素均匀分布在区间 [-20, 20] 上,用于作为函数的输入值
x = torch.linspace(-20, 20, 1000)
y = torch.relu(x)
plt.plot(x,y)
plt.grid()
plt.show()
python 复制代码
x = torch.linspace(-20, 20, 1000, requires_grad=True)
torch.relu(x).sum().backward()
plt.plot(x.detach(),x.grad)
plt.grid()
plt.show()

1.3.4、SoftMax激活函数

softmax用于多分类的输出层 ,它是二分类函数sigmoid在多分类上的推广,目的是将多分类的结果以概率的形式展现出来

  • 二分类(0,1)输出层是一个神经元,输出的是一个概率值(1的概率或者0的概率)。
  • 多分类(0,1,2,3...9),输出应该是类别个数10个神经元,每一个神经元输出属于该类别的概率值,这些概率值之和为1,概率值最大的对应着最后的分类结果。

Tips:回归任务要输出几个值就应该有几个神经元,回归任务输出层没有激活(恒等激活:输入什么输出什么)

计算方法:
s o f t m a x ( z i ) = e z i ∑ j e z j softmax(z_i) = \frac{e^{z_i}}{\sum_je^{z_j}} softmax(zi)=∑jezjezi

  • zi是输出层神经元的加权和
  • 分子是输出层神经元的加权和的e指数
  • 分母是对输出层所有神经元的加权和的e指数来求和
python 复制代码
# 生成输出层神经元的加权和
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)

Softmax 就是将网络输出的 logits 通过 softmax 函数,就映射成为(0,1)的值,而这些值的累和为1 (满足概率的性质),那么我们将它理解成概率,选取概率最大(也就是值对应最大的)节点 ,作为我们的预测目标类别

  • logits/score:输出层神经元的加权和在没有送到激活函数之前的叫法

例如识别手写的数字,10个类别,0-9来识别手写的是数字几。

1.3.5、激活函数的选择

深度学习是有监督学习,主要分为两类任务:分类和回归。

  • 分类分为二分类和多分类**:二分类神经网络的输出层使用sigmoid做激活,多分类神经网络的输出层使用softmax做激活**
  • 回归任务输出层用恒等激活

对于隐藏层:

  • 优先选择ReLU激活函数,需要注意一下Dead ReLU问题, 避免出现大的梯度从而导致过多的神经元死亡
  • 少用使用sigmoid激活函数,可以尝试使用tanh激活函数

对于输出层:

  • 二分类问题选择sigmoid激活函数
  • 多分类问题选择softmax激活函数
  • 回归问题选择恒等激活函数

1.4、参数初始化(w和b)

如上图,在第一层隐藏层,输入的是3个神经元,维度为3,输出的是5个神经元,维度为5,在Pytorch中Linear(3,5)来拟合。在第一层隐藏层中有15个权重Weight、5个偏置Bias

  • 梯度下降的时候要给 weight 和 bias 初始值,这个就有很多方法来给初始值了。

参数初始化方法:

  • 均匀分布初始化: 权重参数初始化从区间均匀随机取值。从均匀分布中随机生成当前神经元的权重,其中 d 为这一层神经元的个数。

均匀分布 : ( − 1 d , 1 d ) d 为这一层隐藏层神经元的个数 , 如上图为 5 均匀分布: (\frac{-1}{\sqrt{d}},\frac{1}{\sqrt{d}}) \\ d为这一层隐藏层神经元的个数,如上图为5 均匀分布:(d −1,d 1)d为这一层隐藏层神经元的个数,如上图为5

  • 正态分布初始化: 随机初始化从均值为0,标准差是1的高斯分布中取样,使用一些二很小的值对参数 w 进行初始化。
  • 全0初始化: 将神经网络中的所有权重参数初始化为0
  • 全1初始化: 将神经网络中的所有权重参数初始化为1
  • 固定值初始化: 将神经网络中的所有权重参数初始化为某个固定值

1.4.1、均匀分布初始化

python 复制代码
# 1. 均匀分布随机初始化权重 w
def test01():
    linear = nn.Linear(3, 5)
    # 均匀分布初始化参数
    nn.init.uniform_(linear.weight)
    print(linear.weight.data)

1.4.2、正态分布初始化

python 复制代码
# 2.标准正态分布初始化
def test02():
    linear = nn.Linear(3, 5)
    nn.init.constant_(linear.weight, 5)
    print(linear.weight.data)
python 复制代码
# 5. 正态分布随机初始化
def test05():
    linear = nn.Linear(5, 3)
    nn.init.normal_(linear.weight, mean=0, std=1)
    print(linear.weight.data)

1.4.3、全0初始化

python 复制代码
# 3. 全0初始化
def test03():
    linear = nn.Linear(3, 5)
    nn.init.zeros_(linear.weight)
    print(linear.weight.data)

1.4.4、全1初始化

python 复制代码
# 4. 全1初始化
def test04():
    linear = nn.Linear(3, 5)
    nn.init.ones_(linear.weight)
    print(linear.weight.data)

1.4.5、kaiming 初始化🔥

凯明,ReLU和kaiming 初始化都是由何凯明提出来的,所以也叫何(HE)初始化。

kaiming 初始化,也叫做 HE 初始化,HE 初始化分为正态分布的 HE 初始化、均匀分布的 HE 初始化。

  • 正态化的he初始化: 就是把正态分布的方差改为(根号下2/输入神经元的个数)

2 f a n _ i n \sqrt{\frac{2}{fan\_in}} fan_in2

  • 均匀分布的he初始化:就是从[-limit,limit]的均匀分布中抽取值进行初始化。

l i m i t = 6 f a n _ i n limit=\sqrt{\frac{6}{fan\_in}} limit=fan_in6

python 复制代码
# 6. kaiming 初始化
def test06():
    # kaiming 正态分布初始化
    linear = nn.Linear(3, 5)
    nn.init.kaiming_normal_(linear.weight)
    print(linear.weight.data)

    # kaiming 均匀分布初始化
    linear = nn.Linear(3, 5)
    nn.init.kaiming_uniform_(linear.weight)
    print(linear.weight.data)

1.4.6、xavier初始化🔥

xavier 初始化,也叫做 Glorot初始化。一种是正态分布的 xavier 初始化、一种是均匀分布的 xavier 初始化。PyTorch框架默认是这种初始化方法。

The 'V'er

  • 正态化的Xavier初始化:就是把正态分布的方差改为(根号下2/输入神经元的个数+输出神经元的个数)

2 f a n _ i n + f a n _ o u t \sqrt{\frac{2}{fan\_in+fan\_out}} fan_in+fan_out2

  • 均匀分布的Xavier初始化:就是从[-limit,limit]的均匀分布中抽取值进行初始化

l i m i t = 6 f a n _ i n + f a n _ o u t limit=\sqrt{\frac{6}{fan\_in+fan\_out}} limit=fan_in+fan_out6

python 复制代码
# 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)
python 复制代码
# pytorch默认就是xavier初始化(简单写法)
linear = nn.Linear(5, 3)
print(linear.weight)
print(linear.bias)

1.5、神经网络的搭建和参数个数计算

在pytorch中定义深度神经网络其实就是层堆叠的过程,继承自nn.Module,实现两个方法:

  • __init__方法中定义网络中的层结构,主要是全连接层,并进行初始化
  • forward方法,在实例化模型的时候,底层会自动调用该函数。该函数中可以定义学习率,为初始化定义的layer传入数据等

init定义层,forward进行层的堆叠

  • forward:前向传播
  • backward:自动微分模块,求所有权重weight的梯度。神经网络的每一层链接实际上就是一个复合函数,对复合函数求微分是从外层到内层,所以backward是从后往前,叫做反向传播。
  • 第1个隐藏层:权重初始化采用标准化的xavier初始化 激活函数使用sigmoid。3个输入,3个输出
    • 参数w个数 = 3×3 = 9,参数b个数 = 3
  • 第2个隐藏层:权重初始化采用标准化的He初始化 激活函数采用relu。3个输入,2个输出
    • 参数w个数 = 3×2 = 6,参数b个数 = 2
  • out输出层: 多分类,激活函数采用softmax。2个输入,2个输出
    • 参数w个数 = 2×2 = 4,参数b个数 = 2
python 复制代码
import torch
import torch.nn as nn
from torchsummary import summary  # 计算模型参数,查看模型结构, pip install torchsummary
# 创建神经网络模型类
class Model(nn.Module):
    # 初始化属性值
    def __init__(self):
        super(Model, self).__init__() # 调用父类的初始化属性值
        self.linear1 = nn.Linear(3, 3) # 创建第一个隐藏层模型, 3个输入特征,3个输出特征
        nn.init.xavier_normal_(self.linear1.weight) # xavier初始化权重w
        # 创建第二个隐藏层模型, 3个输入特征(上一层的输出特征),2个输出特征
        self.linear2 = nn.Linear(3, 2)
        # kaiming初始化权重
        nn.init.kaiming_normal_(self.linear2.weight)
        # 创建输出层模型, 2个输入特征(上一层的输出特征),2个输出特征
        self.out = nn.Linear(2, 2)
        
    # 创建前向传播方法,自动执行forward()方法,x是输入的数据
    def forward(self, x):
        # 数据经过第一个线性层
        x = self.linear1(x)
        # 使用sigmoid激活函数
        x = torch.sigmoid(x)
        # 数据经过第二个线性层
        x = self.linear2(x)
        # 使用relu激活函数
        x = torch.relu(x)
        # 数据经过输出层,输出层是在做加权和,由于是多分类任务,所以需要激活函数softmax
        x = self.out(x)
        # 使用softmax激活函数
        # dim=-1:每一维度行数据相加为1。n个样本,每个样本有2个分数score,[n,2],softmax作用在2最后一维,所以dim为-1
        x = torch.softmax(x, dim=-1)
        return x

if __name__ == "__main__":
    # 实例化model对象(调用init方法)
    my_model = Model()
    # 随机产生数据(5个样本,每个样本3个数据)
    my_data = torch.randn(5, 3)
    print("mydata shape", my_data.shape)
    
    # 数据经过神经网络模型训练(直接传数据,类会自己调用forward方法)
    output = my_model(my_data)
    print("output shape-->", output.shape)   # 输出是5×2
    ##############
    #上述得到的就是初始化的网络,这个时候还没有对网络进行训练,只是把网络搭建好
    ##############
    
    # 计算模型参数
    # 计算每层每个神经元的w和b个数总和
    summary(my_model, input_size=(3,), batch_size=5) 
  • 神经网络的输入数据是为[batch_size, in_features](例如:n个样本,每个样本3个数据)的张量经过网络处理后获取了[batch_size, out_features](例如:n个样本,每个样本2个分类结果)的输出张量。

    • 例如:神经网络输入[n,m],输出[n,k分类]
  • 在上述例子中,batchsize=5, infeatures=3,out_features=2

在真实工程中是不需要进行参数初始化的!如下是真实工程的简约代码

python 复制代码
import torch
import torch.nn as nn
from torchsummary import summary  # 计算模型参数,查看模型结构, pip install torchsummary


# 创建神经网络模型类
class Model(nn.Module):
    # 初始化属性值
    def __init__(self):
        super(Model, self).__init__()
        self.layer1 = nn.Linear(3, 3)
        self.layer2 = nn.Linear(3, 2)
        self.output = nn.Linear(2, 2)

    # 前向传播方法
    def forward(self, x):
        x = torch.relu(self.layer1(x))
        x = torch.relu(self.layer2(x))
        x = self.output(x)   # scores/logits
        out = torch.softmax(x,dim=-1)
        return out


if __name__ == "__main__":
   net = Model()
   x = torch.randn(10,3)
   out = net(x)
   print(out.shape)

   summary(net, input_size=(3,), batch_size=10)

2、损失函数

在深度学习中, 损失函数是用来衡量模型参数的质量的函数, 衡量的方式是比较网络输出和真实输出的差异,损失函数在不同的文献中名称是不一样的,主要有以下几种命名方式:

  • 损失函数:loss function
  • 代价函数:cost function
  • 目标函数:objective function
  • 误差函数:error function

2.1、多分类损失函数(交叉熵损失)

在多分类任务通常使用softmax将logits转换为概率的形式,所以多分类的交叉熵损失也叫做softmax损失,它的计算方法是:

实现方法:

python 复制代码
import torch
import torch.nn as nn

# 分类损失函数:交叉熵损失使用nn.CrossEntropyLoss()实现。
# nn.CrossEntropyLoss()=softmax + 损失计算

# 1、设置真实值: 可以是热编码后的结果也可以不进行热编码
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]], dtype=torch.float32)
# 实例化交叉熵损失
loss = nn.CrossEntropyLoss()
# 计算损失结果
my_loss = loss(y_pred,y_true).numpy()
print(my_loss)

2.2、二分类损失

在处理二分类任务时,我们不再使用softmax激活函数,而是使用sigmoid激活函数,那损失函数也相应的进行调整,使用二分类的交叉熵损失函数:

python 复制代码
import torch
import torch.nn as nn

# 1、设置真实值: 可以是热编码后的结果也可以不进行热编码
y_true = torch.tensor([0, 1, 0], dtype=torch.float32)
# 设置预测值: 预测值是sigmoid输出的结果
y_pred = torch.tensor([0.6901, 0.5459, 0.2469], requires_grad=True)
# 实例化交叉熵损失
criterion = nn.BCELoss()
# 计算损失结果
my_loss = criterion(y_pred,y_true).detach().numpy()
print(my_loss)

2.3、回归损失函数-MAE损失函数

Mean absolute loss(MAE)也被称为L1 Loss,是以绝对误差作为距离。损失函数公式:

  • 回归任务的激活函数为恒等激活,相当于没有激活。

  • 由于L1 loss具有稀疏性,为了惩罚较大的值,因此常常将其作为正则项添加到其他loss中作为约束。

  • L1 loss的最大问题是梯度在零点不平滑,导致会跳过极小值。

MAE损失函数的图像:

python 复制代码
# 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.4、回归损失函数-MSE损失函数

Mean Squared Loss/ Quadratic Loss(MSE loss)也被称为L2 loss,或欧氏距离,它以误差的平方和的均值作为距离损失函数公式:

  • L2 loss也常常作为正则项。
  • 当预测值与目标值相差很大时, 梯度容易爆炸。

MSE损失函数图像为:

python 复制代码
# 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.5、回归损失函数-smooth L1损失函数

smooth L1说的是光滑之后的L1。损失函数公式:

smooth L1损失函数图像如下:

  • 该函数实际上就是一个分段函数
  • 在[-1,1]之间实际上就是L2损失,这样解决了L1的不光滑问题
  • 在[-1,1]区间外,实际上就是L1损失,这样就解决了离群点梯度爆炸的问题
python 复制代码
# 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)

3、梯度下降及其优化

梯度下降法是一种寻找使损失函数最小化的方法。从数学上的角度来看,梯度的方向是函数增长速度最快的方向,那么梯度的反方向就是函数减少最快的方向。

其中学习率太小,那么需要迭代的次数多,如果学习率过大,就有可能直接跳过最优解,进入无限的训练中。解决的方法就是,学习率也需要随着训练的进行而变化

在进行模型训练时,有三个基础的概念:

  • Epoch:使用全部数据对模型进行以此完整训练,训练轮次
  • Batcg_size: 使用训练集中的小部分样本对模型权重进行以此反向传播的参数更新,每次训练每批次样本数量,批大小
  • Iteration: 使用一个 Batch 数据对模型进行一次参数更新的过程,迭代(一次迭代=前向传播+反向传播)

假设数据集有 50000 个训练样本,现在选择 Batch Size = 256 对模型进行训练:

  • 每个 Epoch 要训练的图片数量:50000
  • 训练集具有的 Batch 个数:50000/256+1=196
  • 每个 Epoch 具有的 Iteration 个数:196
  • 10个 Epoch 具有的 Iteration 个数:1960

在深度学习中,梯度下降的几种方式的根本区别就在于 Batch Size不同。神经网络中神经元比较多,网络大,样本都是批量进行训练的。

3.1、前向传播和反向传播

前向传播:指的是数据输入的神经网络中,逐层向前传输,一直到运算到输出层为止

反向传播:利用损失函数 ERROR,从后往前(输出层到输入层),结合梯度下降算法,依次求各个参数的偏导,并进行参数更新。反向传播对神经网络中的各个节点的权重进行更新。


以上图为例,由于是多分类,按道理得用SoftMax激活函数,但是这里我们为了理解反向传播,使用sigmoid激活函数。同时采用最简单的MSE来计算损失函数,一切都是为了理解反向传播算法。

前向传播计算过程:
隐藏层神经元 h 1 的输入 : n e t h 1 = w 1 ∗ i 1 + w 2 ∗ i 2 + b 1 ∗ 1 n e t h 1 = 0.15 ∗ 0.05 + 0.2 ∗ 0.1 + 0.35 ∗ 1 = 0.3775 隐藏层神经元h1的输入:\\ net_{h_1} = w_1 *i_1 + w_2 *i_2 + b_1*1 \\ net_{h_1}=0.15*0.05+0.2*0.1+0.35*1=0.3775 隐藏层神经元h1的输入:neth1=w1∗i1+w2∗i2+b1∗1neth1=0.15∗0.05+0.2∗0.1+0.35∗1=0.3775

隐藏层神经元 h 1 的输出 : o u t h 1 = 1 1 + e − n e t h 1 = 1 1 + e − 0.3775 = 0.593269992 隐藏层神经元h1的输出:\\ out_{h_1} = \frac{1}{1+e^{-net_{h1}}} =\frac{1}{1+e^{-0.3775}} = 0.593269992 隐藏层神经元h1的输出:outh1=1+e−neth11=1+e−0.37751=0.593269992

同理隐藏层神经元h2的输出为:
o u t h 2 = 0.596884378 out_{h_2}=0.596884378 outh2=0.596884378

接下来看输出层:
输出层神经元 o 1 的输入 : n e t o 1 = w 5 ∗ o u t h 1 + w 6 ∗ o u t h 2 + b 2 ∗ 1 n e t o 1 = 0.4 ∗ 0.593269992 + 0.45 ∗ 0.596884378 + 0.6 ∗ 1 = 1.105905967 输出层神经元o1的输入:\\ net_{o_1} = w_5 *out_{h1} + w_6 *out_{h_2} + b_2*1 \\ net_{o_1} = 0.4 *0.593269992 + 0.45 * 0.596884378 + 0.6*1 = 1.105905967 输出层神经元o1的输入:neto1=w5∗outh1+w6∗outh2+b2∗1neto1=0.4∗0.593269992+0.45∗0.596884378+0.6∗1=1.105905967

输出层神经元 o 1 的输出 : o u t o 1 = 1 1 + e − n e t o 1 = 1 1 + e − 1.105905967 = 0.75136507 输出层神经元o1的输出:\\ out_{o_1} = \frac{1}{1+e^{-net_{o1}}} =\frac{1}{1+e^{-1.105905967}} = 0.75136507 输出层神经元o1的输出:outo1=1+e−neto11=1+e−1.1059059671=0.75136507

同理输出层神经元o2的输出为:
o u t o 2 = 0.772928465 out_{o_2}=0.772928465 outo2=0.772928465

算损失函数:
E = ∑ 1 2 ( t a r g e t − o u t p u t ) 2 E = E O 1 + E O 2 = 0.274811083 + 0.023560026 = 0.298371109 E=\sum \frac{1}{2}(target-output)^2 \\ E = E_{O_1} + E_{O_2} = 0.274811083 + 0.023560026 = 0.298371109 E=∑21(target−output)2E=EO1+EO2=0.274811083+0.023560026=0.298371109

上述即为前向传播过程计算过程。


接下来来看反向传播过程:

我们先来求最简单的,求误差E对w5的导数。要求误差E对w5的导数,需要先求误差E对outo1 的导数,再求outo1 对neto1 的导数,最后再求neto1对w5的导数,经过这个处理,我们就可以求出误差E对w5的导数(偏导)。

  • 误差E对outo1 的导数

E = E O 1 + E O 2 = 1 2 ( t a r g e t o 1 − o u t o 1 ) 2 + 1 2 ( t a r g e t o 2 − o u t o 2 ) 2 ∂ E ∂ o u t o 1 = − ( t a r g e t o 1 − o u t o 1 ) = ( − 0.01 − 0.75136507 ) = 0.74136507 E = E_{O_1} + E_{O_2} = \frac{1}{2}(target_{o1}-out_{o1})^2+ \frac{1}{2}(target_{o2}-out_{o2})^2 \\ \frac{\partial E}{\partial out_{o1}} = -(target_{o1}-out_{o1}) = (-0.01-0.75136507)=0.74136507 \\ E=EO1+EO2=21(targeto1−outo1)2+21(targeto2−outo2)2∂outo1∂E=−(targeto1−outo1)=(−0.01−0.75136507)=0.74136507

  • outo1 对neto1 的导数

o u t o 1 = 1 1 + e − n e t o 1 ∂ o u t o 1 ∂ n e t o 1 = − o u t o 1 ( 1 − o u t o 1 ) = 0.75136507 ( 1 − 0.75136507 ) = 0.186815602 out_{o1} = \frac{1}{1+e^{-net_{o1}}} \\ \frac{\partial out_{o1}}{\partial net_{o1}} = -out_{o1}(1-out_{o1}) = 0.75136507(1-0.75136507)=0.186815602 outo1=1+e−neto11∂neto1∂outo1=−outo1(1−outo1)=0.75136507(1−0.75136507)=0.186815602

  • neto1对w5的导数

n e t o 1 = w 5 ∗ o u t h 1 + w 6 ∗ o u t h 2 + b 2 ∗ 1 ∂ n e t o 1 ∂ w 5 = o u t h 1 = 0.593269992 ∂ E ∂ w 5 = 0.74136507 ∗ 0.186815602 ∗ 0.186815602 = 0.082167041 net_{o_1} = w_5 *out_{h1} + w_6 *out_{h_2} + b_2*1 \\ \frac{\partial net_{o1}}{\partial w_{5}} = out_{h1} = 0.593269992 \\ \frac{\partial E}{\partial w_{5}} = 0.74136507 * 0.186815602 * 0.186815602 = 0.082167041 neto1=w5∗outh1+w6∗outh2+b2∗1∂w5∂neto1=outh1=0.593269992∂w5∂E=0.74136507∗0.186815602∗0.186815602=0.082167041

w5原来的值是0.40,现在算出w5的梯度是0.082, 接下来进行参数更新:


导数(梯度)已经计算出来了,下面就是反向传播与参数更新过程
w 5 + = ∂ E ∂ w 5 = w 5 − η ∗ ∂ E ∂ w 5 = 0.4 − 0.5 ∗ 0.082167041 = 0.35891648 w5^+=\frac{\partial E}{\partial w_{5}} = w_5 -η* \frac{\partial E}{\partial w_{5}} = 0.4-0.5*0.082167041 = 0.35891648 w5+=∂w5∂E=w5−η∗∂w5∂E=0.4−0.5∗0.082167041=0.35891648

这样w5原来的值是0.40,现在就被更新为0.35

同理:
w 6 + = 0.408666186 w 7 + = 0.511301270 w 8 + = 0.561370121 w6^+ = 0.408666186 \\ w7^+ = 0.511301270 \\ w8^+ = 0.561370121 w6+=0.408666186w7+=0.511301270w8+=0.561370121

3.2、梯度下降的优化方法

梯度下降优化算法中,可能会碰到以下情况:

  1. 碰到平缓区域,梯度值较小,参数优化变慢
  2. 碰到 "鞍点" ,梯度为 0,参数无法优化
  3. 碰到局部最小值,参数不是最优

对于这些问题, 出现了一些对梯度下降算法的优化方法,例如:Momentum、AdaGrad、RMSprop、Adam 等

3.2.1、梯度下降的优化方法-指数加权平均(动量算法Momentum)

指数移动加权平均则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较小),距离越近则对平均数的计算贡献就越大(权重越大)。比如:明天气温怎么样,和昨天气温有很大关系,而和一个月前的气温关系就小一些。

  • 在普通的梯度下降中,每次更新参数只考虑当前的梯度信息。而指数加权平均则引入了动量的概念,它会在更新参数时考虑过去一段时间内梯度的加权平均值,使得参数更新方向更具稳定性和连贯性。

加权平均计算 :假设当前时刻为 t,第 t 时刻的梯度为 gt,指数加权平均的计算公式为:
v t = β × v t − 1 + ( 1 − β ) g t v_t = β×v_{t-1} + (1-β)g_t vt=β×vt−1+(1−β)gt

其中,Vt是第 t 时刻的指数加权平均梯度(即梯度),β是(0,1)的超参数,通常取值接近于1。得到动量Vt后,使用它来更新模型参数θ,更新公式为:
θ t = θ t − 1 − α V t θ_t = θ_{t-1} - αV_t θt=θt−1−αVt

通过不断迭代上述两个公式,模型参数会根据指数加权平均后的梯度信息进行更新,逐渐收敛到最优解。


Monmentum 优化方法是一定程度上克服平缓和鞍点问题的。

  • 当处于鞍点位置时,由于当前的梯度为 0,参数无法更新。但是 Momentum 动量梯度下降算法已经在先前积累了一些梯度值,很有可能使得跨过鞍点。
  • 由于 mini-batch(小批次梯度下降) 普通的梯度下降算法,每次选取少数的样本梯度确定前进方向,可能会出现震荡,使得训练时间变长。Momentum 使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程
python 复制代码
# 动量算法Momentum
def test():
    # 1、初始化权重参数(创建一个包含单个元素 1.0 的 PyTorch 张量 w,并设置 requires_grad=True,这意味着在后续的计算中,PyTorch 会自动跟踪 w 的梯度信息)
    w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
    # 定义了一个简单的损失函数 y= w²/2,最后使用 sum() 函数对结果求和
    y = ((w ** 2) / 2.0).sum()

    # 2、实例化优化方法:指定优化器小批量梯度下降SGD ,指定参数beta=0.9(动量算法的默认参数是0.9,不用变)
    # [w]:指定需要优化的参数列表,这里只有一个参数 w
    # lr=0.01:设置学习率为 0.01
    # momentum=0.9:设置动量参数为 0.9,动量算法通过引入动量项来加速收敛过程,它会考虑之前的梯度信息,使参数更新具有一定的惯性。
    optimizer = torch.optim.SGD([w], lr=0.01, momentum=0.9)

    # 3、第1次更新 计算梯度,并对参数进行更新
    optimizer.zero_grad()  # 在每次计算梯度之前,需要将之前的梯度信息清零。因为 PyTorch 会累积梯度,如果不清零,后续的梯度计算会受到之前梯度的影响
    y.backward()  # 反向传播计算梯度:调用 backward() 方法计算损失函数 y 关于参数 w 的梯度。PyTorch 会自动使用反向传播算法计算梯度,并将梯度信息存储在 w.grad 中
    optimizer.step()  # 更新梯度:调用优化器的 step() 方法,根据计算得到的梯度和动量算法的规则,对参数 w 进行更新

    # 4、打印第一次更新后的梯度和更新后的权重值
    # w.grad.numpy() 将梯度信息转换为 NumPy 数组,方便打印输出
    # w.detach().numpy() 先使用 detach() 方法将 w 从计算图中分离出来,避免后续的梯度计算影响,然后再转换为 NumPy 数组进行打印
    print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))


    # 5、第二次计算梯度并更新梯度
    y = ((w ** 2) / 2.0).sum()
    optimizer.zero_grad()
    y.backward()
    optimizer.step()
    print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))

3.2.3、梯度下降的优化方法-adaGrad

A D A grad

AdaGrad 通过对不同的参数分量使用不同的学习率,AdaGrad 的学习率总体会逐渐减小。在传统的梯度下降算法中,所有参数在每次更新时都使用相同的固定学习率。然而,不同的参数可能需要不同的学习率。对于经常更新的参数,我们希望学习率能够小一些;而对于更新频率较低的参数,我们希望学习率能够大一些。AdaGrad 算法就能够根据参数的更新频率自适应地调整每个参数的学习率。

  • AdaGrad 缺点是可能会使得学习率过早、过量的降低,导致模型训练后期学习率太小,较难找到最优解。
python 复制代码
# 自适应梯度下降AdaGrad
def test():
    w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
    y = ((w ** 2) / 2.0).sum()

    # 2、实例化优化方法:指定优化器自适应梯度下降Adagrad优化方法
    # [w]:指定需要优化的参数列表,这里只有一个参数 w
    # lr=0.01:设置学习率为 0.01
    optimizer = torch.optim.Adagrad([w], lr=0.01)

    # 3、第1次更新 计算梯度,并对参数进行更新
    optimizer.zero_grad()
    y.backward()
    optimizer.step()

    # 4、打印第一次更新后的梯度和更新后的权重值
    print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))


    # 5、第二次计算梯度并更新梯度
    y = ((w ** 2) / 2.0).sum()
    optimizer.zero_grad()
    y.backward()
    optimizer.step()
    print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))

3.2.4、梯度下降的优化方法-RMSProp

RMSProp 优化算法是对 AdaGrad 的优化. 最主要的不同是,其使用指数移动加权平均梯度替换历史梯度的平方和。

3.2.5、梯度下降的优化方法-Adam

  • Momentum 使用指数加权平均计算当前的梯度值
  • AdaGrad、RMSProp 使用自适应的学习率
  • Adam(自适应矩估计)优化算法将 Momentum 和 RMSProp 算法结合在一起。
    • 修正梯度:使用梯度的指数加权平均
    • 修正学习率:使用梯度平方的指数加权平均
python 复制代码
def test():
    w = torch.tensor([1.0], requires_grad=True, dtype=torch.float32)
    y = ((w ** 2) / 2.0).sum()

    # 2、实例化优化方法:指定优化器Adam算法,其中betas是指数加权的系
    # [w]:指定需要优化的参数列表,这里只有一个参数 w
    # lr=0.01:设置学习率为 0.01
    optimizer = torch.optim.Adam([w], lr=0.01,betas=[0.9,0.99])

    # 3、第1次更新 计算梯度,并对参数进行更新
    optimizer.zero_grad()
    y.backward()
    optimizer.step()

    # 4、打印第一次更新后的梯度和更新后的权重值
    print('第1次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))


    # 5、第二次计算梯度并更新梯度
    y = ((w ** 2) / 2.0).sum()
    optimizer.zero_grad()
    y.backward()
    optimizer.step()
    print('第2次: 梯度w.grad: %f, 更新后的权重:%f' % (w.grad.numpy(), w.detach().numpy()))

3.3、学习率优化

在神经网络训练的初始阶段,模型参数距离最优解通常较远,此时可以使用较大的学习率,让参数快速更新,从而加快模型的收敛速度。然而,随着训练的进行,模型逐渐接近最优解,如果继续使用较大的学习率,参数可能会在最优解附近来回跳动,难以收敛到最优值。因此,需要在适当的时候减小学习率,使模型能够更精确地调整参数,以达到更好的训练效果。

3.3.1、等间隔学习率衰减

等间隔学习率衰减方式如下所示:等间隔学习率衰减就是按照预先设定的间隔,在训练的特定轮数(epoch)后,将学习率按照一定的比例进行衰减。

API为:

python 复制代码
# 功能:等间隔-调整学习率
# step_size:调整间隔数=50
# gamma:调整系数=0.5
# 调整方式:lr = lr * gamma
lr_scheduler.StepLR(optimizer, step_size, gamma=0.1)

3.3.2、指定间隔学习率衰减

API为:

python 复制代码
# milestones:设定调整轮次:[50, 125, 160]
# gamma:调整系数
# 调整方式:lr = lr * gamma
lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1)

3.3.3、按指数学习率衰减

API为:

python 复制代码
# gamma:指数的底
# 调整方式:lr= lr∗ gamma^epoch
lr_scheduler.ExponentialLR(optimizer, gamma)

4、正则化

  • 在设计机器学习算法时希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化。
  • 神经网络的强大的表示能力经常遇到过拟合,所以需要使用不同形式的正则化策略。
  • 目前在深度学习中使用较多的策略有**范数惩罚,DropOut,特殊的网络层(BN层)**等,接下来我们对其进行详细的介绍。

4.1、Dropout正则化

在训练神经网络中模型参数较多,在数据量不足的情况下,很容易过拟合。Dropout(随机失活)是一个简单有效的正则化方法。也就是控制一定比例的神经元死亡,可以缓解过拟合。

一般加了随机失活比不加随机失活准确率能提高2%

  • 如上图,随机失活是对隐藏层随机失活,上图的隐藏层h1 - h5 每个神经元都有概率 p 的几率被失活,可以看到h2和h5被失活置0。剩余的h1、h3、h4三个神经元需要进行放大,即放大为 1/(1-p)。
  • 在测试过程中,随即失活不起作用 。因为随机失活只在训练的过程中对隐藏层使用。
python 复制代码
import torch
import torch.nn as nn

def test():
    # 初始化随机失活层:每个神经元有 40% 的概率被随机失活
    dropout = nn.Dropout(p=0.4)
    # 初始化输入数据:表示某一层的weight信息
    # 生成一个形状为 [1, 4] 的张量,其中的元素是从 0 到 9 的随机整数,且将张量的数据类型转换为浮点数
    inputs = torch.randint(0, 10, size=[1, 4]).float()
    # 创建隐藏层,构建一个全连接层,输入特征维度为 4,输出特征维度为 5。这意味着该层有 4 个输入神经元和 5 个输出神经元
    layer = nn.Linear(4,5)
    # 将输入数据 inputs 传入全连接层 layer 进行前向传播,得到输出 y
    y = layer(inputs)
    print("未失活FC层的输出结果:\n", y) # 打印未经过随机失活处理的全连接层的输出结果

    # 全连接层的输出 y 传入随机失活层 dropout,按照设定的概率(40%)随机将部分元素置为 0
    y =  dropout(y)
    print("失活后FC层的输出结果:\n", y) # 打印经过随机失活处理后的全连接层的输出结果

上述代码将 Dropout 层的概率 p 设置为 0.4,此时经过 Dropout 层计算的张量中就出现了很多 0 , 未变为0的按照(1/(1-0.4))进行处理。

4.2、批量归一化(BN层)

在深度神经网络中,随着网络层数的增加,每一层输入数据的分布会发生变化,这被称为 "内部协变量偏移",这种偏移会使得训练过程变得困难,需要使用较小的学习率和精心设计的初始化方法来保证模型的收敛。批量归一化的核心思想是对每一批次的输入数据进行归一化处理,使得每一层的输入数据分布保持相对稳定,从而加速模型的训练过程。

作用:

  • 加速收敛:通过归一化输入数据,使得梯度在反向传播过程中更加稳定,避免了梯度消失或梯度爆炸的问题,从而可以使用更大的学习率,加快模型的收敛速度
  • 减少对初始化的依赖:由于输入数据的分布相对稳定,模型对初始参数的选择不再那么敏感,降低了初始化的难度
  • 提高模型的泛化能力:在一定程度上起到了正则化的作用,减少了过拟合的风险,提高了模型在测试集上的性能
相关推荐
数字供应链安全产品选型4 小时前
国家级!悬镜安全入选两项“网络安全国家标准应用实践案例”
人工智能·安全·web安全
科技新知4 小时前
大厂AI各走“开源”路
人工智能·开源
字节数据平台4 小时前
火山引擎Data Agent再拓新场景,重磅推出用户研究Agent
大数据·人工智能·火山引擎
TGITCIC4 小时前
LLaVA-OV:开源多模态的“可复现”革命,不只是又一个模型
人工智能·开源·多模态·ai大模型·开源大模型·视觉模型·大模型ai
GeeLark4 小时前
GeeLark 9月功能更新回顾
人工智能
mwq301234 小时前
GPT-2 中的 Pre-Layer Normalization (Pre-LN) 架构详解
人工智能
智奇数美4 小时前
“成本减法”与“效率乘法”——AI智能重构企业通信格局
人工智能·智能手机·信息与通信
技术闲聊DD5 小时前
机器学习(1)- 机器学习简介
人工智能·机器学习
mwq301235 小时前
GPT-2 中的残差权重初始化
人工智能