0


爆肝3W多字,100多张配图!深度学习从小白到精通一篇博文帮你打开人工智能的大门建议收藏不容错过!!!

🤡博客主页:Code_文晓

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!



一.深度学习简介

在介绍深度学习之前,我们先看下这幅图:人工智能>机器学习>深度学习。

深度学习是机器学习的⼀个子集,也就是说深度学习是实现机器学习的一种方法。与机器学习算法的主要区别如下图所示:

传统机器学习算术依赖人工设计特征,并进行特征提取,而深度学习方法不需要人工,而是依赖算法自动提取特征,这也是深度学习被看做黑盒子,可解释性差的原因。

随着计算机软硬件的飞速发展,现阶段通过**拥有众多层数神经网络(Neural Network)**来模拟人脑来解释数据,包括图像,文本,音频等内容。目前来看常用的神经网络包括:

  • 卷积神经网络(Convolutional Neural Network)
  • 循环神经网络(Recurrent Neural Network)
  • 生成对抗网络(Generative Adversarial Networks)
  • 深度强化学习(Deep Reinforcement Learning)等。

二.什么是神经网络

1.什么是神经网络

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

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

2.人工神经网络

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

受生物神经元的启发,人工神经元接收来自其他神经元或外部源的输入每个输入都有一个相关的权值(w),它是根据该输入对当前神经元的重要性来确定的,对该输入加权并与其他输入求和后,经过一个激活函数f计算得到该神经元的输出。这个流程就像,来源不同树突(树突都会有不同的权重)的信息,进行的加权计算,输入到细胞中做加和,再通过激活函数输出细胞值。

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

神经网络中信息只向一个方向移动,即从输入节点向前移动,通过隐藏节点,再向输出节点移动,网络中没有循环或者环。其中的基本构件是:

  1. 输入层: 即输入x的那一层
  2. **输出层:**即输出y的那一层
  3. 隐藏层: 输入层和输出层之间都是隐藏层

特点是:

  • 同⼀层的神经元之间没有连接。
  • 第N层的每个神经元和第N-1层的所有神经元相连(这就是full connected的含义),第N-1层神经元的输出就是第N层神经元的输入。
  • 每个连接都有⼀个权值。

3.神经元是如何工作的

人工神经元接收到一个或多个输入,对他们进行加权并相加,总和通过个非线性函数产生输出。

所有的输入xi,与相应的权重wi相乘并求和:

  • 将求和结果送入到激活函数中,得到最终的输出结果:


三.激活函数

1.网络非线性因素的理解

激活函数用于对每层的输出数据进行变换,进而为整个网络结构结构注入了非线性因素。此时,神经网络就可以拟合各种曲线。如果不使用激活函数,整个网络虽然看起来复杂,其本质还相当于一种线性模型,如下公式所示:

  1. 没有引入非线性因素的网络等价于使用一个线性模型来拟合.
  2. 通过给网络输出增加激活函数实现引入非线性因素,使得网络模型可以逼近任意函数,提升网络对复杂问题的拟合能力.

2.激活函数

在神经元中引入了激活函数,它的本质是向神经网络中引入非线性因素的,通过激活函数,神经网络就可以拟合各种曲线。如果不用激活函数每一层输出都是上层输入的线性函数,无论神经网络有多少层,输出都是输入的线性组合,引入非线性函数作为激活函数,那输出不再是输入的线性组合,可以逼近任意函数。

常用的激活函数有:

2.1 Sigmoid/logistics激活函数

数学表达式为:

函数和其导数图像如下所示:

  • sigmoid 在定义域内处处可导,且两侧导数逐渐趋近于0。如果X的值很大或者很小的时候,那么函数的梯度 (函数的斜率) 会非常小,在反向传播的过程中,导致了向低层传递的梯度也变得非常小。此时,网络参数很难得到有效训练。这种现象被称为梯度消失。
  • 从 sigmoid 函数图像可以得到,sigmoid 函数可以将任意的输入映射到(0,1)之间,当输入的值大致在<-6 或者>6 时,意味着输入任何值得到的激活值都是差不多的,这样会丢失部分的信息。比如:输入100 和输出10000 经过 sigmid 的激活值几乎都是等于1的,但是输入的数据之间相差100倍的信息就丢失了。
  • 对于 sigmoid 函数而言,输入值在[6,6] 之间输出值才会有明显差异,输入值在[3,3]之间才会有比较好的效果。
  • 通过上述导数图像,我们发现导数数值范围是(0,0.25),当输入<-6或者>6时,sigmoid 激活函数图像的导数接近为0,此时网络参数将更新极其缓慢,或者无法更新。
  • 一般来说,sigmoid 网络在 5 层之内就会产生梯度消失现象。而且,该激活函数并不是以0为中心的,所以在实践中这种激活函数使用的很少sigmoid函数一般只用于二分类的输出层。

实现方法:

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

_,axes = plt.subplots(1,2)

# 绘制sigmoid函数图像
x = torch.linspace(-20,20,1000)
y = torch.sigmoid(x)

# 防止标题中文乱码以及刻度不显示负号问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False 

axes[0].plot(x,y)
axes[0].grid
axes[0].set_title('sigmoid 函数图像')

# 绘制sigmoid导数图像
x = torch.linspace(-20,20,1000,requires_grad=True)
y = torch.sigmoid(x).sum().backward() # 转化为标量再进行反向传播

axes[1].plot(x.detach(),x.grad)
axes[1].grid
axes[1].set_title('sigmoid 导数图像')

plt.show()

**运行结果就是上图函数和导数图像,可以自己测试。 **

2.2 tanh激活函数(双曲正切曲线)

数学表达式如下:

tanh的函数和导数曲线图如下所示:

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

实现方法:

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

_,axes = plt.subplots(1,2)

# 绘制sigmoid函数图像
x = torch.linspace(-20,20,1000)
y = torch.tanh(x)

# 防止标题中文乱码以及刻度不显示负号问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False 

axes[0].plot(x,y)
axes[0].grid
axes[0].set_title('tanh 函数图像')

# 绘制sigmoid导数图像
x = torch.linspace(-20,20,1000,requires_grad=True)
y = torch.tanh(x).sum().backward() # 转化为标量再进行反向传播

axes[1].plot(x.detach(),x.grad)
axes[1].grid
axes[1].set_title('tanh 导数图像')

plt.show()

**运行结果就是上图函数和导数图像,可以自己测试一下。 **

2.3 ReLu激活函数

数学表达式为:

ReLu激活函数和其导数曲线如图所示 :

  • ReLU是目前最常用的激活函数。
  • 从上述函数图像可知,ReLU 激活函数将小于0的值映射为 0,而大于0的值则保持不变,它更加重视正信号,而忽略负信号,这种激活函数运算更为简单、能够提高模型的训练效率。但是,如果我们网络的参数采用随机初始化时,很多参数可能为负数,这就使得输入的正值会被舍去,而输入的负值则会保留,这可能在大部分的情况下并不是我们想要的结果。
  • 从图中还能看出,当x<0时,relu导数为0,而当x>0时,则不存在饱和问题。所以,ReLU 能够在x>0时保持梯度不衰减,从而缓解梯度消失问题。然而,随着训练的推进,部分输入会落入小于0区域,导致对应权重无法更新。这种现象被称为“神经元死亡”

** 与sigmoid相比,RELU的优势是:**

  • 采用sigmoid函数,计算量大(指数运算),反向传播求误差梯度时,求导涉及除法,计算量相对大,而采用Relu激活函数,整个过程的计算量节省很多。
  • sigmoid函数反向传播时,很容易就会出现梯度消失的情况,从而无法完成深层网络的训练。
  • Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生。

**实现方法: **

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

_,axes = plt.subplots(1,2)

# 绘制sigmoid函数图像
x = torch.linspace(-20,20,1000)
y = torch.relu(x)

# 防止标题中文乱码以及刻度不显示负号问题
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False 

axes[0].plot(x,y)
axes[0].grid
axes[0].set_title('relu 函数图像')

# 绘制sigmoid导数图像
x = torch.linspace(-20,20,1000,requires_grad=True)
y = torch.relu(x).sum().backward() # 转化为标量再进行反向传播

axes[1].plot(x.detach(),x.grad)
axes[1].grid
axes[1].set_title('relu 导数图像')

plt.show()

**运行结果就是上图函数和导数图像,可以自己测试一下。 **

2.4 Leaky ReLu激活函数

该激活函数是对RELU的改进,数学表达式为:

Leaky ReLu激活函数和其导数曲线如图所示 :

Leaky Relu激活函数相比relu激活函数的优点是什么?

  1. 避免“神经元死亡”问题:当ReLU中的输入为负数时,输出为0,此时该神经元就不再参与后续计算,称为“神经元死亡”。而Leaky ReLU在输入为负数时会有一个小的斜率,可以使神经元不至于完全失活,从而避免“神经元死亡”问题。
  2. 更好的收敛性: ReLU 在输入为负数时梯度为0,这可能会导致梯度消失或梯度爆炸等问题。而Leaky ReLU由于有一个小的斜率,因此可以解决这个问题,使得模型训练更加稳定且收敛更快。

2.5 SoftMax

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

计算方法如下图所示:

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

import torch
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt

# 数字中的score
x = torch.tensor([0.2,0.02,0.15,1.3,0.5,0.06,1.1,0.05,3.75])
# 将其送⼊到softmax中计算分类结果
y = torch.softmax(x,dim=0)
# 将结果进⾏打印
print(y)

2.6 其它激活函数

2.7 激活函数的思考

这么多激活函数,我们应该如何选择呢?

对于隐藏层

  • 优先选择RELU激活函数。
  • 如果ReLu效果不好,那么尝试其他激活,如Leaky ReLu等。
  • 如果你使用了Relu,需要注意一下Dead Relu问题,避免出现大的梯度从而导致过多的神经元死亡。
  • 不要使用sigmoid激活函数,可以尝试使用tanh激活函数。

** 对于输出层**

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

四.网络参数初始化

对于某一个神经元来说,需要初始化的参数有两类:一类是权重W,还有一类是偏置b,偏置b初始化为0即可。而权重W的初始化比较重要,我们着重来介绍常见的初始化方式。

**以下初始化方式代码开头都需要导入以下库,后面不重复导入。 **

import torch
import torch.nn.functional as F
import torch.nn as nn

1.随机初始化(标准正态分布初始化)

随机初始化从均值为0,标准差是1的高斯分布中取样,使用一些很小的值对参数W进行初始化。

#随机初始化(标准正态分布初始化) 
#nn.init.normal_(linear.weight,均值=0,方差=1)  
#可以省略“均值”“方差”二字

linear = nn.Linear(5,3)
nn.init.normal_(linear.weight,mean=0, std=1)
print(linear.weight)

2.均匀分布初始化(标准初始化)

权重参数初始化从区间均匀随机取值。即在(-1/√d,1/√d)均匀分布中生成当前神经元的权重,其中d为每个神经元的输入数量。

# 均匀分布初始化 nn.init.constant_(linear.weight)

# 创建一个线性层,输入数据特征维度是5,输出维度是3
linear = nn.Linear(5,3)

# 对linear线性层 均匀分布初始化,init:初始化 uniform_:均匀分布 参数:linear.weight
nn.init.uniform_(linear.weight)
print(linear.weight)

3.Xavier初始化

该方法的基本思想是各层的激活值和梯度的方差在传播过程中保持一致,也叫做Glorot初始化。初始化方法有两种:

  1. 标准正态分布的Xavier初始化:Glorot 正态分布初始化器,也称为 Xavier 正态分布初始化器。它从以 0为中⼼,标准差为 stddev = sqrt(2 / (fan_in + fan_out)) 的正态分布中抽取样本, 其中 fan_in 是输⼊神经元的个数, fan_out是输出的神经元个数。
  2. 均匀分布的Xavier初始化:Glorot 均匀分布初始化器,也称为 Xavier 均匀分布初始化器。它从 [-limit,limit] 中的均匀分布中抽取样本, 其中 limit 是 sqrt(6 /(fan_in + fan_out)) , 其中 fan_in 是输⼊神经元的个数,fan_out 是输出的神经元个数。
# xavier初始化

# 1.正态分布的xavier初始化 nn.init.xavier_normal_(linear.weight)
linear = nn.Linear(5,3)
nn.init.xavier_normal_(linear.weight)
print(linear.weight)

# 2.均匀分布的xavier初始化 nn.init.xavier_uniform_(linear.weight)
linear = nn.Linear(5,3)
nn.init.xavier_uniform_(linear.weight)
print(linear.weight)

4.He初始化

He初始化,也称为Kaiming初始化,出自大神何恺明之手,它的基本思想是正向传播时,激活值的方差保持不变;反向传播时,关于状态值的梯度的方差保持不变。初始化方法也有两种:

  1. 标准正态分布的he初始化:He 正态分布初始化是以 0 为中心,标准差为stddev=sqrt(2/fan_in) 的截断正态分布中抽取样本, 其中 fan_in 是输⼊神经元的个数
  2. 均匀分布的he初始化:He 均匀⽅差缩放初始化器。它从 [-limit,limit] 中的均匀分布中抽取样本, 其中 limit 是 sqrt(6 / fan_in) , 其中 fan_in 输⼊神经元的个数。
# Kaiming初始化 

#1.正态分布的Kaiming初始化 nn.init.kaiming_normal_(linear.weight)
linear = nn.Linear(5,3)
nn.init.kaiming_normal_(linear.weight)
print(linear.weight)

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

5.全0、全1和固定初始化

# 1.固定初始化  nn.init.constant_(linear.weight, 固定值)
linear = nn.Linear(5,3)
nn.init.constant_(linear.weight,5)
print(linear.weight)

# 2.全0初始化 nn.init.zeros_(linear.weight)
linear = nn.Linear(5,3)
nn.init.zeros_(linear.weight)
print(linear.weight)

# 3.全1初始化 nn.init.ones_(linear.weight)
linear = nn.Linear(5,3)
nn.init.ones_(linear.weight)
print(linear.weight)

总结:

神经网络中的偏置可以初始化为0,但是权重参数不能初始化为0。
一般不会手动初始化参数,pytorch和tensorfloe默认都会有自己的合适的初始化方式,除非效果不好的时候可以初始化一下参数。


五.神经网络的搭建

接下来我们来构建如下图所示的神经网络模型:

首先我们介绍下用来构建网络的全连接层

PyTorch中的全连接层(Fully Connected Layer),也被称为线性层(Linear Layer),是深度学习中最基本、常用的一种神经网络层。其作用是将输入数据进行矩阵变换,实现特征提取和分类等功能。一般情况下,全连接层会在特征提取部分后紧接着使用.

函数接口为:

torch.nn.Linear(in_features: int, out_features: int, bias: bool = True, device=None, dtype=None)

一般常用的参数就两个

torch.nn.Linear(in_features= , out_features= ,bias= );

全连接层的参数意义如下:

  • in_features:输入数据张量的大小,也就是输入数据的特征数。比如,如果输入的是一张28 * 28像素的灰度图像,那么输入数据的特征数就是28 * 28 = 784。
  • out_features: 输出数据张量的大小,也就是全连接层神经元的数量。比如,如果想训练一个10类别的分类器,那么输出数据的张量大小应该是10。
  • bias:是否添加偏置项。偏置项是一个常数向量,用于增加模型的表达能力。默认情况下,PyTorch的全连接层会自动添加偏置项。

torch.nn.Linear()构建全连接神经网络有两种方式:

  1. 自定义神经网络类:这是一种更灵活的方式,它允许你自定义每个层的行为,并实现更高级的功能,如跳跃连接、条件计算等等。你可以通过继承nn.Module类来创建自定义神经网络。
  2. 使用nn.Sequential():这是一种简单的方式,其中每个层都按照顺序依次链接。nn.Linear()可以使用函数定义线性层,然后将其添加到Sequential中以创建一个完整的模型。

1.自定义神经网络类:

import torch
import torch.nn as nn

# 定义神经网络类
class Net(nn.Module):
    def __init__(self):
        super(Net,self).__init__()

        # 定义层1(输入层到隐藏层)
        self.fc1 = nn.Linear(784, 512)
        # 定义激活函数1
        self.relu1 = nn.ReLU()
        # 定义层2(隐藏层到隐藏层)
        self.fc2 = nn.Linear(512, 256)
        # 定义激活函数2
        self.relu2 = nn.ReLU()
        # 定义层3(隐藏层到输出层)
        self.fc3 = nn.Linear(256, 10)

    # 定义前向传播方法
    def forward(self,x):
        # 展平图片数据,变成(batch_size, size)的二维张量
        x = x.view(-1, 784)
        # 输入层到隐藏层:(batch_size, 784) -> (batch_size, 512)
        x = self.fc1(x)
        # 激活函数1:(batch_size, 512) -> (batch_size, 512)
        x = self.relu1(x)
        # 隐藏层到隐藏层:(batch_size, 512) -> (batch_size, 256)
        x = self.fc2(x)
        # 激活函数2:(batch_size, 256) -> (batch_size, 256)
        x = self.relu2(x)
        # 隐藏层到输出层: (batch_size, 256) -> (batch_size, 10)
        x = self.fc3(x)
        return x

# 实例化神经网络
model = Net()

print(model)

2.使用nn.Sequential():

import torch
import torch.nn as nn

# 定义神经网络类
class Net(nn.Module):
    def __init__(self):
        super(Net,self).__init__()

        # 使用Sequential快捷构造神经网络
        self.fc = nn.Sequential(
            nn.Linear(784, 512),    # 输入层到隐藏层
            nn.ReLU(),              # 非线性激活函数
            nn.Linear(512, 256),    # 隐藏层到隐藏层
            nn.ReLU(),
            nn.Linear(256, 10)      # 隐藏层到输出层
        )

    # 定义前向传播方法
    def forward(self,x):
        # 展平图片数据,变成(batch_size, size)的二维张量
        x = x.view(-1, 784)
        # 完整前向传播
        x = self.fc(x)
        return x

# 实例化神经网络
model = Net()

print(model)

3.神经网络的优缺点

1.优点

  • 精度高,性能优于其他的机器学习方法,甚至在某些领域超过了人类。
  • 可以近似任意的非线性函数
  • 随之计算机硬件的发展,近年来在学界和业界受到了热捧,有大量的框架和库可供调用

**2.缺点 **

  • 黑箱,很难解释模型是怎么工作的。
  • 训练时间长,需要大量的计算力。
  • 网络结构复杂,需要调整超参数。
  • 小数据集上表现不佳,容易发生过拟合。

六.损失函数

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

1.分类任务

在深度学习的分类任务中使用最多的是交叉熵损失函数,所以在这里我们着重介绍这种损失函数。

1.1 多分类任务

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

其中,y是样本x属于某一个类别的真实概率,而f(x)是样本属于某一类别的预测分数,S是softmax函数,L用来衡量p,q之间差异性的损失结果。

例子:

上图中的交叉熵损失为:

从概率角度理解,我们的目的是,最小化 正确类别所对应的预测概率的对数 的负值,如下图所示:

在torch.nn中使用CrossEntropyLoss()实现,如下所示:

import torch
import torch.nn as nn

# 设置真实值和预测值
y_true = torch.tensor([[0, 1, 0], [0, 0, 1]], dtype=torch.float32)
y_pred = torch.tensor([[0.05, 0.9, 0.05], [0.05, 0.05, 0.9]], dtype=torch.float32)

# 创建损失函数对象
ce_loss_fn = nn.CrossEntropyLoss()

# 计算损失
ce_loss = loss_fn(y_pred, y_true)

# 打印损失值
print(loss.item()) #0.6177929639816284

1.2 二分类任务

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

其中,y是样本x属于某一个类别的真实概率,而y^是样本属于某一类别的预测概率,L用来衡量真实值与预测值之间差异性的损失结果。

在torch.nn中使用BCELoss()实现,如下所示:

import torch
import torch.nn as nn

# 设置真实值和预测值
y_true = torch.tensor([[0], [1]], dtype=torch.float32)
y_pred = torch.tensor([[0.4], [0.6]], dtype=torch.float32)

# 创建损失函数对象
loss_fn = nn.BCELoss()

# 计算损失
bce_loss = loss_fn(y_pred, y_true)

# 打印损失值
print(bec_loss.item()) #0.5108255743980408

2.回归任务

回归任务中常用的损失函数有以下几种:

2.1 MAE损失

Mean absolute loss(MAE)也被称为L1Loss,是以绝对误差作为距离。

曲线如下图所示:

特点是: 由于L1Loss具有稀疏性,为了惩罚较大的值,因此常常将其作为正则项添加到其他loss中作为约束。L1Loss的最大问题是梯度在零点不平滑,在0点处不可导,导致会跳过极小值。

在torch.nn中使用L1Loss()实现,如下所示:

import torch
import torch.nn as nn

# 设置真实值和预测值的张量
y_true = torch.tensor([[0.], [0.]])  # 真实值
y_pred = torch.tensor([[1.], [1.]])  # 预测值

# 创建损失函数对象
loss_fn = nn.L1Loss()  # 使用L1Loss作为MAE损失函数

# 计算损失
L1_loss = loss_fn(y_pred, y_true)

# 打印损失值
print(L1_loss.item()) #1.0

2.2 MSE损失

Mean Squared Loss/ Quadratic Loss(MSE loss)也被称为L2 loss,或欧氏距离,它以误差的平方和作为距离:

曲线如下图所示:

特点是: L2loss也常常作为正则项。当预测值与目标值相差很大时梯度容易爆炸。

在torch.nn中使用L2Loss()实现,如下所示:

import torch
import torch.nn as nn

# 设置真实值和预测值的张量
y_true = torch.tensor([[0.], [0.]])  # 真实值
y_pred = torch.tensor([[1.], [1.]])  # 预测值

# 创建损失函数对象
loss_fn = nn.MSELoss()  # 使用L1Loss作为MAE损失函数

# 计算损失
mse_loss = loss_fn(y_pred, y_true)

# 打印损失值
print(mse_loss.item()) #1.0

2.3 smooth L1 损失

Smooth L1损失函数如下式所示:

其中:x=f(x)-y 为真实值和预测值的差值。

从上图中可以看出,该函数实际上就是一个分段函数,在[-1,1]之间实际上就是L2损失,这样解决了L1的不光滑问题,在[-1,11]区间外,实际上就是L1损失,这样就解决了离群点梯度爆炸的问题。通常在目标检测中使用该损失函数。

在torch.nn中使用SmoothL1Loss()实现,如下所示:

import torch
import torch.nn as nn

# 设置真实值和预测值的张量
y_true = torch.tensor([[0.], [1.]])  # 真实值
y_pred = torch.tensor([[0.2], [0.6]])  # 预测值

# 创建损失函数对象
loss_fn = nn.SmoothL1Loss()  # 使用L1Loss作为MAE损失函数

# 计算损失
SL1_loss = loss_fn(y_pred, y_true)

# 打印损失值
print(SL1_loss.item()) #0.04999999701976776

总结:

  • 知道分类任务的损失函数

多分类的交叉熵损失函数和⼆分类的交叉熵损失函数

  • 知道回归任务的损失函数

MAE,MSE,smooth L1损失函数


七.梯度下降和反向传播算法

1.梯度下降算法

梯度下降法简单来说就是一种寻找使损失函数最小化的方法。大家在机器学习阶段已经学过该算法,所以我们在这里就简单的回顾下,从数学上的角度来看,梯度的方向是函数增长速度最快的方向,那么梯度的反方向就是函数减少最快的方向,所以有:

其中,n是学习率,如果学习率太小,那么每次训练之后得到的效果都太小,增大训练的时间成本。如果,学习率太大,那就有可能直接跳过最优解,进入无限的训练中。解决的方法就是,学习率也需要随着训练的进行而变化。

在上图中我们展示了一维和多维的损失函数,损失函数呈碗状。在训练过程中损失函数对权重的偏导数就是损失函数在该位置点的梯度。我们可以看到,沿着负梯度方向移动,就可以到达损失函数底部,从而使损失函数最小化。这种利用损失函数的梯度迭代地寻找局部最小值的过程就是梯度下降的过程。

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

  1. **Eoch: **使用全部数据对模型进行以此完整训练。
  2. **Batch:**使用训练集中的小部分样本对模型权重进行以此反向传播的参数更新
  3. iteration: 使用一个Batch 数据对模型进行一次参数更新的过程。

实际上,梯度下降的几种方式的根本区别就在于 Batch Size不同,,如下表所示:

注:上表中Mini-Batch 的 Batch 个数为 N/B+1是针对未整除的情况。整除则是N/B。

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

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

实际中使用较多的是小批量的梯度下降算法。 在torch.optim通过以下⽅法实现:

# 定义优化器,设置学习率和动量参数
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

2.反向传播算法(BP算法)

  • 多层神经网络的学习能力比单层网络强得多。想要训练多层网络,需要更强大的学习算法。误差反向传播算法(Back Propagation)是其中最杰出的代表,它是目前最成功的神经网络学习算法。现实任务使用神经网络时,大多是在使用 BP 算法进行训练,值得指出的是BP算法不仅可用于多层前馈神经网络,还可以用于其他类型的神经网络。通常说BP网络时,一般是指用BP算法训练的多层前馈神经网络。
  • 利用反向传播算法对神经网络进行训练。该方法与梯度下降算法相结合对网络中所有权重计算损失函数的梯度,并利用梯度值来更新权值以最小化损失函数。在介绍BP算法前,我们先看下前向传播与链式法则的内容。

2.1 前向传播与反向传播的概念

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

在网络的训练过程中经过前向传播后得到的最终结果跟训练样本的真实值总是存在一定误差,这个误差便是损失函数。想要减小这个误差,就用损失函数ERROR,从后往前,依次求各个参数的偏导,这就是**反向传播(Back Propagation)**。

BP (Back Propagation)算法也叫做误差反传播算法,它用于求解模型的参数梯度,从而使用梯度下降法来更新网络参数。它的基本工作流程如下:

  1. 通过正向传播得到误差,所谓正向传播指的是数据从输入到输出层,经过层层计算得到预测值,并利用损失函数得到预测值和真实值之前的误差
  2. 通过反向传播把误差传递给模型的参数,从而对网络参数进行适当的调整,缩小预测值和真实值之间的误差。
  3. 反向传播算法是利用链式法则进行梯度求解,然后进行参数更新。对于复杂的复合函数,我们将其拆分为一系列的加减乘除或指数,对数,三角函数等初等函数,通过链式法则完成复合函数的求导。

2.2 链式法则

反向传播算法是利用链式法则进行梯度求解及权重更新的。对于复杂的复合函数,我们将其拆分为一系列的加减乘除或指数,对数,三角函数等初等函数,通过链式法则完成复合函数的求导。

为简单起见,这里以一个神经网络中常见的复合函数的例子来说明这个过程。复合函数f(x;w,b)为:

其中x是输入数据,w是权重,b是偏置。我们需要求关于w和b的偏导,然后应用梯度下降公式就可以更新参数。我们将复合函数分解为一系列的初等函数导数相乘的形式:

并进行图形化表示,如下所示:

整个复合函数f(x;wb)关于参数 w 和b的导数可以通过 (x;w,b) 与参数w和b之间路径上所有的导数连乘来得到,即:

以w为例,当x = 1,w = 0,b= 0 时,可以得到

注意:常用函数的导数:


2.3 反向传播算法举例(重点)

在网络的训练过程中经过前向传播后得到的最终结果跟训练样本的真实值总是存在一定误差,这个误差便是损失函数。想要减小这个误差,就用损失函数ERROR,从后往前,依次求各个参数的偏导,这就是**反向传播(Back Propagation)**。

BP (Back Propagation)算法也叫做误差反传播算法,它用于求解模型的参数梯度,从而使用梯度下降法来更新网络参数。它的基本工作流程如下:

  1. 通过正向传播得到误差,所谓正向传播指的是数据从输入到输出层,经过层层计算得到预测值,并利用损失函数得到预测值和真实值之前的误差
  2. 通过反向传播把误差传递给模型的参数,从而对网络参数进行适当的调整,缩小预测值和真实值之间的误差。
  3. 反向传播算法是利用链式法则进行梯度求解,然后进行参数更新。对于复杂的复合函数,我们将其拆分为一系列的加减乘除或指数,对数,三角函数等初等函数,通过链式法则完成复合函数的求导。

这里举两个例子,两种表现形式,来讲解神经网络如何进行前向传播,反向传播,以及BP算法进行更新网络参数的过程:

例1 :

为了能够把计算过程描述的更详细一些,上图中一个矩形代表一个神经元,每个神经元中分别是值和激活值的计算结果和其对应的公式,最终计算出真实值和预测值之间的误差 0.2984.其中:

  1. 由下向上看,最下层绿色的两个圆代表两个输入值
  2. 右侧的8个数字,最下面4个表示 w1、w2、w3、w4 的参数初始值,最上面的4个数字表示w5、w6、w7、w8 的参数初始值
  3. b1 值为 0.35,b2值为 0.60
  4. 预测结果分别为:0.7514、0.7729

我们首先计算w5和w7两个权重的梯度,然后使用梯度下降更新这两个参数。

计算出了梯度值,接下来使用使用梯度下降公式来更新模型参数,假设:学习率为 0.5,则:

接下来,我们计算 w1的梯度,以及更新该参数:

接下来更新该参数:

其他的网络参数更新过程和上面的过程是一样的。下面我们使用代码构建上面的网络,并进行一次正向传播和反向传播。

import torch
import torch.nn as nn
import torch.optim as optim

# 1.搭建神经网络模型
class Net(nn.Module):
    
    def __init__(self):
        # 注意:必须手动调用父类的初始化函数
        super(Net,self).__init__()
        
        # 构建全连接层神经网络
        self.linear1 = nn.Linear(in_features=2,out_features=2)
        self.linear2 = nn.Linear(in_features=2,out_features=2)
       
        # 本案例手动对网络参数进行初始化
        self.linear1.weight.data = torch.tensor([[0.15,0.20],[0.25,0.30]])
        self.linear2.weight.data = torch.tensor([[0.40,0.45],[0.50,0.55]])
        self.linear1.bias.data = torch.tensor([0.35,0.35])
        self.linear2.bias.data = torch.tensor([0.60,0.60])
        
    # forward函数是进行正向传播的函数,在该函数中会定义定向传播的计算过程
    def forward(self,x):
        
        x = self.linear1(x)
        x = torch.sigmoid(x)
        x = self.linear2(x)
        x = torch.sigmoid(x)
        
        # 正向传播结束之后,需要返回输出结果
        return x

# 2.反向传播

# 输入数据,注意:二维列表表示批次样本的输入
inputs = torch.tensor([[0.05,0.10]])
# 真实值
target = torch.tensor([[0.01,0.99]])

# 初始化网络对象
net =Net()
# 输出值
output = net(inputs) # 这么写相当于直接调用了对象内部的 forward函数
# print(output)  #tensor([[0.7514, 0.7729]], grad_fn=<SigmoidBackward0>)

# 计算误差
loss = torch.sum((output - target)**2) / 2
# print(loss) #tensor(0.2984, grad_fn=<DivBackward0>)

# 构建优化器(梯度下降)
optimizer = optim.SGD(net.parameters(),lr=0.5) #net.parameters()获得所有的参数

# 梯度清零
optimizer.zero_grad()

# 反向传播
loss.backward()

# 参数更新
optimizer.step()

# 打印参数的梯度
print(net.linear1.weight.grad)
print(net.linear2.weight.grad)

# 打印更新之后的参数值
print(net.state_dict())

**例2: **

如下图是一个简单的神经网络用来举例:激活函数为sigmoid

前向传播运算:

接下来是**反向传播 (求网络误差对各个权重参数的梯度) **

我们先来求最简单的,求误差E对w5的导数。首先明确这是一个“链式法则”的求导过程,要求误差E对w5的导数,需要先求误差E对out o1的导数,再求out o1对net o1的导数,最后再求net o1对w5的导数,经过这个链式法则,我们就可以求出误差E对w5的导数(偏导),如下图所示:

导数(梯度) 已经计算出来了,下面就是**反向传播与参数更新过程: **

如果要想求误差E对w1的导数,误差E对w1的求导路径不止一条,这会稍微复杂一点,但换汤不换药,计算过程如下所示:

至此,“反向传播算法”的过程就讲完了啦!


八.梯度下降优化算法

优化方法是对传统的梯度下降算法进行优化,传统的梯度下降优化算法在进行网络训练时,可能会碰到以下情况:

  • 碰到平缓区域,梯度值较小,参数优化变慢;
  • 碰到“鞍点”,梯度为0,参数无法优化;
  • 碰到局部最小值;

对于这些问题,我们怎么改进SGD呢?这里就有一些对梯度下降算法的优化方法,例如: Momentum、AdaGrad、RMSprop、Adam等。

前言:指数加权平均数计算方法

在介绍优化算法之前先了解知道一下指数加权平均算法,因为后续的梯度下降优化算法都是基于这个思想的。我们最常见的:

  • 算数平均指的是将所有数加起来除以数的个数,每个数的权重是相同的;
  • 加权平均指的是给每个数赋予不同的权重求得平均数;
  • 移动平均数指的是计算最近邻的 N 个数来获得平均数。
  • 指数移动加权平均则是参考各数值,并且各数值的权重都不同,距离越远的数字对平均数计算的贡献就越小(权重较小),距离越近则对平均数的计算贡献就越大(权重越大)。

假设给定一个序列,例如北京一年每天的气温值,图中蓝色的点代表真实数据。

这时温度值波动比较大,那我们就使用加权平均值来进行平滑,如下图红线就是平滑后的结果:

再举一个例子:

比如:明天气温怎么样,和昨天气温有很大关系,而和一个月前的气温关系就小一些。

计算公式可以用下面的式子来表示:

  1. St为 t 指数加权平均后的值。
  2. Yt为 t 时刻时的真实值。
  3. β为权重值,该值越大平均数越平缓。

如上图所示,加权平均值进行平滑后的图中β设为0.9,那么指数加权平均的计算结果为:

  • S1=Y1
  • S2=0.9S1 +0.1Y2
  • ......
  • S99 = 0.9S98 +0.1Y99
  • S100 = 0.9S99 +0.1Y100
  • ......

那么第100天的结果就可以表示为:

**代码: **

1.没有指数加权平均

import torch
import matplotlib.pyplot as plt

# 随机产生近30天的气温数据

#1.没有指数加权平均
#固定随机数种子
torch.manual_seed(0)
# 随机产生30天的温度
temperature = torch.randn(size=[30, ]) * 10
# 绘制平均温度值
days = torch.arange(1, 31,1)
plt.plot(days,temperature,'o-r')
plt.show()

2.有指数加权平均

#2.有指数加权平均

#固定随机数种子
torch.manual_seed(0)
#随机产生30天的温度
temperature = torch.randn(size=[30, ]) * 10
#绘制平均温度值
days = torch.arange(1, 31,1)

# 存储历史指数加权平均值
exp_weight_avg = []
# 设置权重系数beta
beta = 0.9
for idx, temp in enumerate(temperature,1):
    
    if idx ==1:
        exp_weight_avg.append(temp)
        continue
    
    new_temp = exp_weight_avg[idx-2] * beta + (1 - beta) *temp
    exp_weight_avg.append(new_temp)
    
# 绘制指数加权平均温度值
days = torch.arange(1,31,1)
plt.plot(days,exp_weight_avg,'o-r')
plt.show()

1.Momentum(动量梯度下降算法)

”平缓”、“鞍点”区域时,参数更新速度变慢,Momentum通过指数加权平当梯度下降碰到“峡谷”、均法,累计历史梯度值,进行参数更新,越近的梯度值对当前参数更新的重要性越大。

**梯度计算公式为: **

  1. St-1 表示历史梯度移动加权平均值
  2. wt 表示当前时刻的梯度值
  3. B为权重系数

咱们举个例子,假设:权重β为0.9,例如:

  1. w 表示初始梯度
  2. d 表示当前轮数计算出的梯度值
  3. s 表示历史梯度值

梯度下降公式中梯度的计算,就不再是当前时刻t的梯度值,而是历史梯度值的指数移动加权平均值。公式修改为:

与原始的梯度下降算法相比,它的下降趋势更平滑。那么,Monmentum 优化方法是如何一定程度上克服“平缓”、”鞍点”、”峡谷”的问题呢?

  1. 当处于鞍点位置时,由于当前的梯度为 0,参数无法更新。但是 Momentum 动量梯度下降算法已经在先前积累了一些梯度值,很有可能使得跨过鞍点。
  2. 由于 mini-batch 普通的梯度下降算法,每次选取少数的样本梯度确定前进方向,可能会出现震荡,使得训练时间变长。Momentum 使用移动加权平均,平滑了梯度的变化,使得前进方向更加平缓,有利于加快训练过程。一定程度上有利于降低“峡谷”问题的影响。
  3. 峡谷问题:就是会使得参数更新出现剧烈震荡。
  4. Momentum算法可以理解为是对梯度值的一种调整,我们知道梯度下降算法中还有一个很重要的学习率,Momentum 并没有学习率进行优化。

2.AdaGrad

AdaGrad 通过对不同的参数分量使用不同的学习率,AdaGrad 的学习率总体会逐渐减小,这是因为AdaGrad 认为: 在起初,我们距离最优目标仍较远,可以使用较大的学习率,加快训练速度,随着迭代次数的增加,距离最优目标越来越近,学习率也随之逐渐下降。

其计算步骤如下:

  1. 初始化学习率 α、初始化参数 θ、小常数 σ = 1e-6
  2. 初始化梯度累积变量 s = 0
  3. 从训练集中采样 m 个样本的小批量,计算梯度g
  4. 累积平方梯度 s = s + g ⊙ g,⊙表示各个分量相乘

5.学习率 α 的计算公式如下:

6.参数更新公式如下:

7.重复2-6步骤。
AdaGrad 缺点是:可能会使得学习率过早、过量的降低,导致模型训练后期学习率太小,较难找到最优解。

3.RMSprop

AdaGrad算法在迭代后期由于学习率过小,能较难找到最优解。为了解决这一问题,RMSProp算法对AdaGrad算法做了一点小小的优化。最主要的不同是,其使用指数移动加权平均梯度替换历史梯度的平方和。其计算过程如下:

  1. 初始化学习率 α、初始化参数 θ、小常数 σ = 1e-6
  2. 初始化参数 θ
  3. 初始化梯度累计变量 s
  4. 从训练集中采样 m 个样本的小批量,计算梯度g
  5. 使用指数移动平均累积历史梯度,公式如下:

6.学习率 a 的计算公式如下:

7.参数更新公式如下:

RMSProp与AdaGrad 最大的区别是对梯度的累积方式不同,对于每个梯度分量仍然使用不同的学习率。

RMSProp 通过引入衰减系数 β,控制历史梯度对历史梯度信息获取的多少.被证明在神经网络非凸条件下的优化更好,学习率衰减更加合理一些。

需要注意的是:AdaGrad和 RMSProp 都是对于不同的参数分量使用不同的学习率,如果某个参数分量的梯度值较大,则对应的学习率就会较小,如果某个参数分量的梯度较小,则对应的学习率就会较大一些。

4.Adam

Momentum 使用指数加权平均计算当前的梯度值AdaGrad、RMSProp****使用自适应的学习率Adam 结合了 Momentum、RMSProp 的优点,使用: 移动加权平均的梯度和移动加权平均的学习率。使得能够自适应学习率的同时,也能够使用Momentum 的优点。

Adam 优化算法 (Adaptive Moment Estimation,自适应矩估计) 将Momentum 和 RMSProp 算法结合在一起。Adam算法在RMSProp算法基础上对小批量随机梯度也做了指数加权移动平均。

5.小结

本小节主要学习了常见的一些对普通梯度下降算法的优化方法,主要有 Momentum、AdaGrad、RMSProp、Adam等优化方法,其中 Momentum 使用指数加权平均参考了历史梯度,使得梯度值的变化更加平缓。AdaGrad 则是针对学习率进行了自适应优化,由于其实现可能会导致学习率下降过快,RMSProp对AdaGrad 的学习率自适应计算方法进行了优化,Adam 则是综合Momentum和RMSProp 的优点,**在很多场景下,Adam 的表示都很不错。 **


九.学习率退火

在训练神经网络时,一般情况下学习率都会随着训练而变化,这主要是由于,在神经网络训练的后期,如果学习率过高,会造成loss的振荡,但是如果学习率减小的过快,又会造成收敛变慢的情况。

1.分段常数衰减

分段常数衰减是在事先定义好的训练次数区间上,设置不同的学习率常数。刚开始学习率大一些,之后越来越小,区间的设置需要根据样本量调整,一般样本量越大区间间隔应该越小。

2.指数衰减

指数衰减可以⽤如下的数学公式表示:

其中,t表示迭代次数,α0,k是超参数。

3.1/t 衰减

1/t衰减可以⽤如下的数学公式表示:

其中,t表示迭代次数,α0,k是超参数


十.深度学习正则化

在设计机器学习算法时不仅要求在训练集上误差小,而且希望在新样本上的泛化能力强。许多机器学习算法都采用相关的策略来减小测试误差,这些策略被统称为正则化。因为神经网络的强大的表示能力经常遇到过拟合,所以需要使用不同形式的正则化策略。
正则化通过对算法的修改来减少泛化误差,目前在深度学习中使用较多的策略有参数范数惩罚,提前终止,DropOut等,接下来我们对其进行详细的介绍。

1.Dropout正则化

1.在训深层练神经网络时,由于模型参数较多,在数据量不的情况下,很容易过拟合。Dropout 就是在神经网络中一种缓解过拟合的方法。

2.我们知道,缓解过拟合的方式就是降低模型的复杂度,而 Dropout 就是通过减少神经元之间的连接,把稠密的神经网络神经元连接,变成稀疏的神经元连接,从而达到降低网络复杂度的目的。

3.Dropout是在深度学习领域最常用的正则化技术。Dropout的原理很简单:假设我们的神经网络结构如下所示,在每个迭代过程中,随机选择某些节点,并且删除前向和后向连接。

因此,每个迭代过程都会有不同的节点组合,从而导致不同的输出,这可以看成机器学习中的集成方法 (ensemble technique) 。集成模型一般优于单一模型,因为它们可以捕获更多的随机性。相似地,dropout使得神经网络模型优于正常的模型。

在torch.nn中实现dropout的方法是:nn.Dropout(p = rate);rate:每⼀个神经元被丢弃的概率。

1.创建以及使用dropout:

import torch
import torch.nn as nn

# 初始化Dropout对象
dropout = nn.Dropout(p=0.8)
# 初始化数据
inputs = torch.randint(0,10,size=[5,8]).float()
print(inputs)

print(50*'-')

# 将inputs输入经过dropout,每个输入的数据会有 p 的公概率设置为0
inputs = dropout(inputs)
print(inputs)

结果为:

**说明: **我们将 Dropout 层的概率 p 设置为 0.8,此时经过 Dropout 层计算的张量中就出现了很多0,概率p 设置值越大,则张量中出现的0就越多。上面结果的计算过程如下:

  1. 先按照p设置的概率,随机将部分的张量元素设置为 0
  2. 为了校正张量元素被设置为0带来的影响,需要对非0的元素进行缩放,其缩放因子为:1 / (1 -p)。上面代码中p的值为 0.8根据公式缩放因子为:1/(1-0.8)=5
  3. 比如:第3个元素,原来是 5,乘以缩放因子之后变成 25

我们也发现了,丢弃概率 p 的值越大,则缩放因子的值就越大,相对其他未被设置的元素就要更多的变大。丢弃概率 P 的值越小,则缩放因子的值就越小,相对应其他未被置为 0的元素就要有较小的变大。
当张量某些元素被设置为0时,对网络会带来什么影响?
比如上面这种情况,如果输入该样本,会使得某些参数无法更新,请看下面的代码:

**2.dropout 随机丢弃对网络参数的影响 **

import torch
import torch.nn as nn

# 2.dropout 随机丢弃对网络参数的影响

## 2.1 未经过dropout的梯度值用作对照

# 固定随机种子
torch.manual_seed(0)

# 初始化权重参数
w = torch.randn(15,1,requires_grad=True)
# 随机初始化数据
x = torch.randint(0,10,size=[5,15]).float()

y = x @ w
y = y.sum() # 转换为张量
# 计算梯度
y.backward()
print('未经过dropout的梯度值:',w.grad.reshape(1,-1).squeeze().numpy())

print(50*'-')

# 2.1 经过dropout的梯度值

# 固定随机种子
torch.manual_seed(0)

# 初始化权重参数
w = torch.randn(15,1,requires_grad=True)
# 随机初始化数据
x = torch.randint(0,10,size=[5,15]).float()

# 初始化丢弃层
dropout = nn.Dropout(p=0.8)
x = dropout(x)

y = x @ w
y = y.sum() # 转换为张量
# 计算梯度
y.backward()
print('经过dropout后的梯度值:',w.grad.reshape(1,-1).squeeze().numpy())

由此可知 dropout之后的数据会有一定概率为0.从而降低网络复杂度,具有正则化作用。

2.L1与L2正则化

L1和L2是最常见的正则化方法。它们在损失函数 (cost function) 中增加一个正则项,由于添加了这个正则化项,权重矩阵的值减小,因为它假定具有更小权重矩阵的神经网络导致更简单的模型。因此,它也会在一定程度上减少过拟合。然而,这个正则化项在L1和L2中是不同的。

  • L2正则化

这里的入是正则化参数,它是一个需要优化的超参数。L2正则化又称为权重衰减,因为其导致权重趋向于0(但不全是0。

  • L1正则化

这里,我们惩罚权重矩阵的绝对值。其中,λ 为正则化参数,是超参数不同于L2,权重值可能被减少到0.因此,L1对于压缩模型很有用。其它情况下,一般选择优先选择L2正则化。

3.提前停止

提前停止 (early stopping) 是将一部分训练集作为验证集 (validationset)。当验证集的性能越来越差时或者性能不再提升,则立即停止对该模型的训练。这被称为提前停止。

在上图中,在虚线处停止模型的训练,此时模型开始在训练数据上过拟合。

4.批量归一化

在神经网络的搭建过程中,Batch Normalization (批量归一化)是经常使用一个网络层,其主要的作用是控制数据的分布,加快网络的收敛。

我们知道,神经网络的学习其实在学习数据的分布,随着网络的深度增加、网络复杂度增加,一般流经网络的数据都是一个 mini batch,每个 mini batch 之间的数据分布变化非常剧烈,这就使得网络参数频繁的进行大的调整以适应流经网络的不同分布的数据,给模型训练带来非常大的不稳定性使得模型难以收敛。

如果我们对每一个 mini batch 的数据进行标准化之后,数据分布就变得稳定,参数的梯度变化也变得稳定,有助于加快模型的收敛。

**批量归一化公式: **

  • λ和β是可学习的参数,它相当于对标准化后的值做了一个线性变换,λ为系数,β为偏置;
  • ε (eps) 通常指为 1e-5,是一个非常小的数,是为了避免分母为 0;
  • E(x)表示变量的均值;
  • Var(x)表示变量的方差;

数据在经过 BN 层之后,无论数据以前的分布是什么,都会被归一化成均值为 β,标准差为γ的分布。

注意:BN 层不会改变输入数据的维度,只改变输入数据的的分布.在实际使用过程中,BN 常常和卷积神经网络结合使用,卷积层的输出结果后接 BN 层。

PyTorch中BN层的接口:

nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  1. momentum:由于每次使用的 minibatch 的数据集,所以BN 使用移动加权平均来近似计算均值和方差,而momentum 参数则调节移动加权平均值的计算;
  2. num_features:表示输入的特征图中包含的特征数量(即通道数)。
  3. affine:一个布尔值,控制是否对缩放和偏移系数进行学习。affine = False 表示 γ=1,β=0,反之,则表示 γ 和 β 要进行学习,默认为True;
  4. BatchNorm2d 适用于输入的数据为 4D,输入数据的形状[N,C,H,W]
  5. track_running_stats:一个布尔值,控制是否在训练时跟踪输入数据的均值和方差,默认为 True,在训练过程中会统计每个 batch 的均值和方差,然后计算整体的均值和方差。

其中:N 表示批次,C代表通道数,H 代表高度,W 代表宽度。

由于每次输入到网络中的时小批量的样本,我们使用指数加权平均来近似表示整体的样本的均值和方法,其更新公式如下:

running_mean = momentum * running_mean + (1.0 - momentum) * batch_mean
running_var = momentum * running_var + (1.0 - momentum) * batch_var

上面的式子中,batch_mean 和 batch_var 表示当前批次的均值和方差。而running_mean 和running_var 是近似的整体的均值和方差的表示。当我们进行评估时,可以使用该均值和方差对输入数据进行归一化。

代码示例:

# 输入的形状:[batch size,channel,height,width]
inputs = torch.randint(0,10,[2,2,3,3]).float()
print(inputs)

print('-'*50)

#num features 表示每个样本特征图的数量,通道数量
#affine为 False 表示不带 gama 和 beta 两个学习参数
#eps 小常数,避免分母为 0
bn = nn.BatchNorm2d(num_features=2, affine=False, eps=1e-5)
result = bn(inputs)
print(result)

#均值是每个样本对应通道的均值
#方差对应通道的方差

小结:本小节学习了批量归一化层,该层的作用主要是用来控制每层数据的流动时的均值和方差,防止训练过程出现剧烈的波动,模型难以收敛,或者收敛较慢。批量归一化层在计算机视觉领域使用较多。


十一.卷积神经网络

利用全连接神经网络对图像进行处理存在以下两个问题:

1.需要处理的数据量大,效率低假如我们处理一张 1000x 1000 像素的图片,参数量如下1000x1000x3=3,000,000这么大量的数据处理起来是非常消耗资源的。

2.图像在维度调整的过程中很难保留原有的特征,导致图像处理的准确率不高。

假如有圆形是1,没有圆形是0,那么圆形的位置不同就会产生完全不同的数据表达。但是从图像的角度来看,图像的内容 (本质) 并没有发生变化,只是位置发生了变化。所以当我们移动图像中的物体,用全连接升降得到的结果会差异很大,这是不符合图像处理的要求的。

1.CNN网络概述

卷积神经网络是深度学习在计算机视觉领域的突破性成果.在计算机视觉领域,往往我们输入的图像都很大,使用全连接网络的话,计算的代价较高.另外图像也很难保留原有的特征,导致图像处理的准确率不高
卷积神经网络 (Convolutional Neural Network) 是含有卷积层的神经网络卷积层的作用就是用来自动学习、提取图像的特征
CNN网络主要有三部分构成: 卷积层、池化层和全连接层构成,其中卷积层负责提取图像中的局部特征,池化层用来大幅降低参数量级(降维);全连接层类似人工神经网络的部分,用来输出想要的结果。

**讲解卷积神经网络知识之前很有必要学习一下图像的基础知识,毕竟卷积神经网络主要用于图像上。 **

2.图像基础知识

  • 我们在进行图像任务时,需要了解图像的基础知识。图像是由像素点组成的,每个像素点的值范围为:[0,255],像素值越大意味着较亮。比如一张 200x200 的图像,则是由 40000 个像素点组成,如果每个像素点都是0的话意味着这是一张全黑的图像。
  • 我们看到的彩色图一般都是多通道的图像,所谓多通道可以理解为图像由多个不同的图像层叠加而成例如我们看到的彩色图像一般都是由 RGB三个通道组成的,还有一些图像具有RGBA四个通道,最后一个通道为透明通道,该值越小,则图像越透明。

**2.1像素和通道的理解 **

import torch
import numpy as np
import matplotlib.pyplot as plt

# 1.像素点的理解

# 构建200 x 200,像素值全为0的图像
img = np.zeros([200,200])
print(img)
plt.imshow(img, cmap='gray',vmin=0,vmax=255)
plt.show()

# 构建200 x 200,像素值全为255的图像
img = np.full([200,200],255)
print(img)
plt.imshow(img, cmap='gray',vmin=0,vmax=255)
plt.show()

# 2.对图像通道的理解

# 从磁盘中读取彩色图片
img = plt.imread('彩色图片.jpg')
print(img.shape)  # (500,500,3)表示图片有3个通道RGB

img = np.transpose(img,[2,0,1])
print(img.shape) 

for channel in img:
    print(channel)
    plt.imshow(channel)
    plt.show()

3.卷积层

CNN网络受人类视觉神经系统的启发,人类的视觉原理: 从原始信号摄入开始(瞳孔摄入像素 Pixels),接着做初步处理 (大脑皮层某些细胞发现边缘和方向),然后抽象 (大脑判定,眼前的物体的形状,是圆形的)然后进一步抽象 (大脑进一步判定该物体是只人脸)。下面是人脑进行人脸识别的一个示例:

CNN网络主要有三部分构成: 卷积层、池化层和全连接层构成,其中卷积层负责提取图像中的局部特征;池化层用来大幅降低参数量级(降维);全连接层类似人工神经网络的部分,用来输出想要的结果。

整个CNN网络结构如下图所示:

3.1 卷积运算

在计算机视觉领域,往往我们输入的图像都很大,使用全连接网络的话,计算的代价较高.另外图像也很难保留原有的特征,导致图像处理的准确率不高。
卷积神经网络 (Convolutional Neural Network) 是含有卷积层的神经网络卷积层的作用就是用来自动学习、提取图像的特征。

接下来,我们开始学习卷积核的计算过程,即: 卷积核是如何提取特征的。

卷积层是卷积神经网络中的核心模块,卷积层的目的是提取输入特征图的特征,如下图所示,卷积核可以提取图像中的边缘信息。

那么,它是如何进行计算的呢?

  1. input 表示输入的图像
  2. filter 表示卷积核也叫做滤波器
  3. input 经过 filter 的得到输出为最右侧的图像,该图叫做特征图

**卷积运算本质上就是在滤波器和输入数据的局部区域间做点积。 **

3.2 padding

通过上面的卷积计算过程,我们发现最终的特征图比原始图像小很多,如果想要保持经过卷积后的图像大小不变,可以在原图周围添加 padding 来实现。

**3.3 **stride

按照步长为1来移动卷积核,计算特征图如下所示:

如果我们把stride增大,比如设为2,也是可以提取特征图的,如下图所示:

3.4 多通道卷积

实际中的图像都是多个通道组成的,我们怎么计算卷积呢?

计算方法如下:

当输入有多个通道 (channel) 时(例如图片可以有 RGB三个通道),卷积核需要拥有相同的channel数,每个卷积核 channel 与输入层的对应 channel 进行卷积,将每个 channel 的卷积结果按位相加得到最终的 Feature Map。

3.5 多卷积核卷积

如果有多个卷积核时怎么计算呢? 当有多个卷积核时,每个卷积核学习到不同的特征,对应产生包含多个 channel 的 Feature Map,例如下图有两个 filter,所以 output 有两个 channel。

3.6 特征图大小

输出特征图的大小与以下参数息息相关:

  • size:卷积核/过滤器大小,一般会选择为奇数,比如有:11,33,5*5
  • padding:零填充的方式
  • stride:步长

计算方法如下所示:

1.输入体积大小H1 W1 D1

2.四个超参数:

  • Filter数量K;
  • Filter大小F;
  • 步长S;
  • 零填充大小P

3.输出体积大小H2* W2* D2:

  • H2 =(H1 -F+2P)/S +1
  • W2 =(W1 - F+2P)/S +1
  • D2=K

输入特征图为5x5,卷积核为3x3,外加padding 为1,则其输出尺寸为:

(5-3+2*1) / 1 + 1 = 5

如下图所示:

在torch.nn中卷积核的实现使用:

import torch.nn as nn

nn.Conv2d(in_channels, out_channels, kernel_size, stride=stride, padding=padding)

pyTorch卷积的API使用例子:单个卷积核

import torch
import torch.nn as nn
import matplotlib.pyplot as plt

def show(img):
    
    # 要求输入图像的形状:(H,W,C)
    img = plt.imshow(img)
    plt.axis('off') # 去掉坐标系刻度标签
    plt.show()
    
#读取图像:(500,500,3)--> (H,W,C)
img = plt.imread('彩色图片.jpg') # 这里放入相应的图片
print(img.shape)
show(img)
# 1.单个卷积核

#构建卷积核
# in channels 输入图像的通道数
# out channels 指的是当输入一个图像之后,产生几个特征图。也可以理解为卷积核的数量
# kernel size 表示卷积核大小
# stride 步长
# padding 填充
conv = nn.Conv2d(in_channels=3, out_channels=1, kernel_size=3, stride=1, padding=1)

# 额外注意:卷积层对输入的数据有形状要求,(Batchsize,channel,Height,Width)
#将 (H,W,C)-> (C,H,W)
img = torch.tensor(img).permute(2,0,1)
print(img.shape)

# 将(C,H,W)-> (B,C,H,W)
new_img = img.unsqueeze(0)
print(new_img.shape)

# 将数据送到卷积层计算
new_img = conv(new_img.to(torch.float32))
print(new_img.shape)

# 将((B,C,H,W)-> (H,W,C)
new_img = new_img.squeeze(0).permute(1,2,0)
show(new_img.detach().numpy())

运行结果:

pyTorch卷积的API使用例子:多个卷积核

## 2.多个卷积核
#读取图像:(500,500,3)--> (H,W,C)
img = plt.imread('彩色图片.jpg')
# out channels = 3指的是当输入一个图像之后,产生3个特征图,卷积核的数量为3
conv = nn.Conv2d(in_channels=3, out_channels=3, kernel_size=3, stride=1, padding=1)

# 注意:卷积层对输入的数据有形状要求:(Batchsize,channel,Height,Width)
#将 (H,W,C)-> (C,H,W)
img = torch.tensor(img).permute(2,0,1)
print(img.shape)

# 将(C,H,W)-> (B,C,H,W)
new_img = img.unsqueeze(0)
print(new_img.shape)

# 将数据送到卷积层计算
new_img = conv(new_img.to(torch.float32))
print(new_img.shape)

# 将((B,C,H,W)-> (H,W,C)
new_img = new_img.squeeze(0).permute(1,2,0)
print(new_img.shape)

show(new_img[:,:,0].detach().numpy())
show(new_img[:,:,1].detach().numpy())
show(new_img[:,:,2].detach().numpy())

运行结果:

4.池化层

池化层(Pooling)降低维度,缩减模型大小,提高计算速度并提高了Feature Map 的鲁棒性,防止过拟合。

即主要对卷积层学习到的特征图进行下采样(SubSampling)处理,池化层主要有两种:

1.单通道最大池化和平均池化

Max Pooling,取窗口内的最大值作为输出,这种方式使用较广泛。

最大池化:

  1. max(4, 9, 5, 6)
  2. max(2, 5, 2, 4)
  3. max(2, 4, 5, 6)
  4. max(5, 4, 8, 4)

Avg Pooling,取窗口内的所有值的均值作为输出

平均池化:

  1. mean(4, 9, 5, 6)
  2. mean(2, 5, 2, 4)
  3. mean(2, 4, 5, 6)
  4. mean(5, 4, 8, 4)

2.多通道最大池化和平均池化

在处理多通道输入数据时,池化层对每个输入通道分别池化,而不是像卷积层那样将各个通道的输入相加。这意味着池化层的输出和输入的通道数是相等。

**PyTorch池化API使用: **

# 1.最大池化
maxpool = nn.MaxPool2d(kernel_size=2,stride=1,padding=0)
# 2.平均池化
avgpool = nn.AvgPool2d(kernel_size=2,stride=2,padding=1)

5.全连接层

全连接层位于CNN网络的末端,经过卷积层的特征提取与池化层的降维后,将特征图转换成一维向量送入到全连接层中进行分类或回归的操作。其实就是前十章的内容。


十二.循环神经网络基础

1.RNN概述

自然语言处理 (Naturelanguage Processing,NLP)研究的主要是通过计算机算法来理解自然语言。对于自然语言来说,处理的数据主要就是人类的语言,例如: 汉语、英语、法语等,由于该类型的数据不像我们前面接触的过的结构化数据、或者图像数据可以很方便的进行数值化。所以,在本章节,我们主要学习如何将文本数据进行数值化的词嵌入技术、以及如何对文本数据建模的循环网络模型。

最后,我们通过使用学习到的技术完成文本生成任务,即: 输入一个词,由模型预测出指定长度的
歌词。

1.1 词嵌入层的使用

词嵌入层首先会根据输入的词的数量构建一个词向量矩阵,例如:我们有100个词,每个词希望转换成128 维度的向量,那么构建的矩阵形状即为:100*128,输入的每个词都对应了一个该矩阵中的一个向量。

在 PyTorch 中,我们可以使用 nn.Embedding 词嵌入层来实现输入词的向量化。接下来,我们将会学习如何将词转换为词向量,其步骤如下:

  1. 先将语料进行分词,构建词与索引的映射,我们可以把这个映射叫做词表,词表中每个词都对应了一个唯一的索引。
  2. 然后使用 nn.Embedding 构建词嵌入矩阵,词索引对应的向量即为该词对应的数值化后的向量表示。

例如,我们的文本数据为:"北京冬奥的进度条已经过半,不少外国运动员在完成自己的比赛后踏上归途。",接下来,我们看下如何使用词嵌入层将其进行转换为向量表示,步骤如下:

  1. 首先,将文本进行分词;
  2. 然后,根据词构建词表;
  3. 最后,使用嵌入层将文本转换为向量表示

nn.Embedding 对象构建时,最主要有两个参数:

  1. num_embeddings 表示词的数量
  2. embedding_dim 表示用多少维的向量来表示每个词
nn.Embedding(num_embeddings=10,embedding_dim=4)

接下来,我们就实现下刚才的需求:

词嵌入层-Embedding使用:

import torch
import torch.nn as nn
import jieba

if __name__ == '__main__':
    text = '北京东奥的进度条已经过半,不少外国运动员在完成自己的比赛后踏上归途。'
    
    # 1.分词
    words = jieba.lcut(text)
    print(words)
    
    # 2.构建词表
    index_to_word = {} #给定一个索引,能够查询到该索引对应的词
    word_to_index = {} #给定一个词,能够查询到该词对应的索引
    
    #去重
    unique_words = list(set(words))
    
    for idx, word in enumerate(unique_words):
        index_to_word[idx] = word
        word_to_index[word] = idx
    
    # 3.构建词嵌入层
    embed = nn.Embedding(num_embeddings=len(index_to_word), embedding_dim=4) # 维度一般为128,256等。这里用4仅作为演示
    
    # 4.将文本转换为词向量表示(将句子数值化)
    
    #print(index_to_word[0])
    #print(embed(torch.tensor(0)))
    for word in words:
        # 获得词对应的唯一的索引
        idx = word_to_index[word]
        # 根据唯一的索引找到找到该词的向量表示
        word_vec = embed((torch.tensor(idx)))
        print('%3s\t' % word, word_vec)

1.2 关于词嵌入层的思考

我们的词嵌入层默认使用的是均值为 0,标准差为1的正态分布进行初始化,也可以理解为是随机初始化。有些同学可能就想,这个用来表示词的文本真的能够表达出词的含义吗?

  1. nn.Embedding 中对每个词的向量表示都是随机生成的
  2. 当一个词输入进来之后,会使用随机产生的向量来表示该词
  3. 该词向量参与到下游任务的计算
  4. 下游任务计算之后,会和目标结果进行对比产生损失
  5. 接下来,通过反向传播更新所有的网络参数,这里的参数就包括了nn.Embedding中的词向量表示

这样通过反复的前向计算、反向传播、参数更新,最终我们每个词的向量表示就会变得更合理

1.3 小结

本小节主要讲解了在自然语言处理任务中,经常使用的词嵌入层的使用。它的主要作用就是将输入的词映射为词向量,便于在网络模型中进行计算。
这里需要注意的是,词嵌入层中的向量表示是可学习的,并不是固定不变的。

2.RNN理解

我们前面学习了词嵌入层,可以将文本数据映射为数值向量,进而能够送入到网络进行计算。但是,还存在一个问题,文本数据是具有序列特性的,例如:"我爱你”这串文本就是具有序列关系的,"爱”需要在“我”之后,"你”需要在“爱”之后,如果颠倒了顺序,那么可能就会表达不同的意思。

为了能够表示出数据的序列关系我们需要使用循环神经网络(Recurrent Nearal Networks,RNN)来对数据进行建模,RNN 是一个具有记忆功能的网络,它作用于处理带有序列特点的样本数据。

本小节,我们将会带着大家深入学习 RNN 循环网络层的原理、计算过程,以及在 PyTorch 中如何使用RNN 层。

2.1 RNN网络原理

当我们希望使用循环网络来对“我爱你"进行语义提取时,RNN 是如何计算过程是什么样的呢?

上图中h 表示隐藏状态,每一次的输入都会有包含两个值:上一个时间步的隐藏状态、当前状态的输入值,输出当前时间步的隐藏状态。

上图中,为了更加容易理解,虽然我画了3个神经元,但是实际上只有一个神经元,"我爱你"三个字是重复输入到同一个神经元中。

接下来,我们举个例子来理解上图的工作过程,假设我们要实现文本生成,也就是输入“我爱”这两个字,来预测出“你",其如下图所示:

我们将上图展开成不同时间步的形式,如下图所示:

我们首先初始化出第一个隐藏状态,一般都是全0的一个向量,然后将“我”进行词嵌入,转换为向量的表示形式,送入到第一个时间步,然后输出隐藏状态 h1,然后将 h1 和“爱”输入到第二个时间步,得到隐藏状态 h2,将 h2 送入到全连接网络,得到“你”的预测概率。

那么,你可能会想,循环网络只能有一个神经元吗?我们的循环网络网络可以有多个神经元,如下图所示:

我们依次将“我爱你”三个字分别送入到每个神经元进行计算,假设词嵌入时,"我爱你”的维度为128,经过循环网络之,"我爱你”三个字的词向量维度就会变成 4.所以,我们理解了循环神经网络的的神经元个数会影响到输出的数据维度.

**每个神经元内部是如何计算的呢? **

上述公式中:

  1. Wi 表示输入数据的权重
  2. bi 表示输入数据的偏置
  3. Whh表示输入隐藏状态的权重
  4. bhh 表示输入隐藏状态的偏置

最后对输出的结果使用 tanh 激活函数进行计算,得到该神经元你的输出。

**PyTorch RNN 层的使用: **

接下来,我们学习 PyTorch 的 RNN 层的用法
注意: RNN 层输入的数据为三个维度:(sec_len,batch_size,input_size)

**1.RNN输入单个词: **

# 1.RNN输入单个词

#初始化RNN网络层
# input_size 输入句子的每个词的向量的维度,比如:'我' 经过了词嵌入,得到了一个 128 维的向量表示
# hidden_size 隐藏层的大小,隐藏层的神经元的个数,影响到最终输出数据的维度
rnn = nn.RNN(input_size=128, hidden_size=256)

# 初始化输入数据
#注意输入的数据有两个:上一个时间步的隐藏状态、当前时间步的输入
#inputs 的形状 (seq_len,batch_size,input_size)
inputs = torch.randn(1,1,128)
# 隐藏层的形状 (num_layers,batch_size,hidden_size)
# 初始的隐藏状态全部为 0
hn = torch.zeros(1, 1,256)

# 将数据送入到循环网络层
# 输出:output,hn
output, hn = rnn(inputs,hn)
print(output.shape)  #torch.Size([1, 1, 256])
print(hn.shape) # torch.Size([1, 1, 256])

2.RNN输入句子:

# 2.RNN输入句子

#初始化RNN网络层
rnn = nn.RNN(input_size=128, hidden_size=256)

# 构造输入数据
# inputs的形状 (seq_len,batch_size,input_size)
# 输入的句子长度为8,一次输入1个句子
inputs = torch.randn(8,1,128)

# 隐藏层的形状 (num_layers,batch_size,hidden_size)
# 初始的隐藏状态全部为 0
hn = torch.zeros(1,1,256)

# 将数据送入到循环网络层
# 输出:output,hn
output, hn = rnn(inputs,hn)
print(output.shape) # torch.Size([8, 1, 256])
print(hn.shape) # torch.Size([1, 1, 256])

**3.RNN输入批次的句子: **

# 3.RNN输入批次的句子

#初始化RNN网络层
rnn = nn.RNN(input_size=128, hidden_size=256)

# 构造输入数据
# inputs的形状 (seq_len,batch_size,input_size)
# 输入的句子长度为8,一次输入1个句子
inputs = torch.randn(8,16,128)

# 隐藏层的形状 (num_layers,batch_size,hidden_size)
# 初始的隐藏状态全部为 0
hn = torch.zeros(1,16,256)

# 将数据送入到循环网络层
# 输出:output,hn
output, hn = rnn(inputs,hn)
print(output.shape)# torch.Size([8, 16, 256])
print(hn.shape) #torch.Size([1, 16, 256])

本文转载自: https://blog.csdn.net/weixin_43382136/article/details/130714147
版权归原作者 醉竺 所有, 如有侵权,请联系我们删除。

“爆肝3W多字,100多张配图!深度学习从小白到精通一篇博文帮你打开人工智能的大门建议收藏不容错过!!!”的评论:

还没有评论