神经网络

神经网络的基本概念

  • 神经网络: 神经网络是由多个层组成的模型,每一层由多个神经元(或节点)构成。神经网络能够学习输入数据中的复杂模式,并进行预测或分类。
  • 前向传播: 在神经网络中,输入数据通过各层进行传递和计算,直到输出层生成结果的过程称为前向传播。
  • 反向传播: 训练过程中,通过计算损失函数相对于网络参数的梯度,使用优化算法调整网络参数的过程称为反向传播。

1. torch.nn 模块

torch.nn 是 PyTorch 中专门用于构建神经网络的模块,提供了各种层、损失函数和优化器的实现。

2. 神经网络层

2.1. 常见的神经网络层
  • 线性层 (Linear): 实现线性变换。

    import torch.nn as nn
    
    linear_layer = nn.Linear(in_features=10, out_features=5)
    
  • 卷积层 (Conv): 用于处理图像数据,提取局部特征。

    conv_layer = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3)
    
  • 池化层 (Pooling): 降低特征维度,保留重要信息。

    pooling_layer = nn.MaxPool2d(kernel_size=2)
    
  • 循环神经网络层 (RNN): 用于处理序列数据。

    rnn_layer = nn.RNN(input_size=10, hidden_size=20)
    
  • LSTM 层: 长短期记忆网络,用于更复杂的序列数据。

    lstm_layer = nn.LSTM(input_size=10, hidden_size=20)
    
2.2. 激活函数

激活函数用于引入非线性,常用的激活函数包括:

  • ReLU (Rectified Linear Unit):

    relu = nn.ReLU()
    
  • Sigmoid:

    sigmoid = nn.Sigmoid()
    
  • Tanh:

    tanh = nn.Tanh()
    
2.3. 正则化层
  • Dropout: 随机丢弃一部分神经元,以减少过拟合。

    dropout = nn.Dropout(p=0.5)  # 50% 的概率丢弃
    

3. 损失函数

损失函数用于衡量模型输出与真实标签之间的差距,常见的损失函数有:

  • 均方误差损失 (MSELoss): 适用于回归任务。

    loss_function = nn.MSELoss()
    
  • 交叉熵损失 (CrossEntropyLoss): 适用于多类分类任务。

    loss_function = nn.CrossEntropyLoss()
    

4. 优化器

优化器负责更新模型的参数以最小化损失函数。PyTorch 提供了多种优化器:

  • 随机梯度下降 (SGD):

    optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
    
  • Adam:

    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    

卷积

卷积层(Convolutional Layers)是深度学习中卷积神经网络(CNN)的核心组件,特别是在图像处理和计算机视觉任务中非常常见。卷积层的主要功能是通过提取局部特征来学习数据中的空间层次结构。以下是关于卷积层的详细说明:

  1. 卷积层的基本概念

卷积层通过应用多个卷积核(或过滤器)对输入数据进行操作,以提取特征。卷积核是一组可学习的参数,通常是小的矩阵(如 3x3 或 5x5)。这些卷积核在输入数据上滑动(即“卷积”),并计算点积,从而生成一个特征图(feature map)。

  1. 工作原理

    卷积核不停的在原图上进行滑动,对应元素相乘再相加。下图为每次滑动移动1格,然后再利用原图与卷积核上的数值进行计算得到缩略图矩阵的数据,如下图右所示。
    在这里插入图片描述

  • 输入:卷积层的输入通常是多维数组,例如图像(通常为3D:高度、宽度和通道)。

  • 卷积操作:卷积核在输入数据上滑动,并在每个位置执行点积操作,将结果存储在特征图中。

  • 激活函数:卷积层后通常跟一个激活函数(如ReLU),以引入非线性特征。

  • 步幅(Stride):控制卷积核移动的步长。例如,步幅为1表示每次移动1个像素,步幅为2表示每次移动2个像素,从而减小特征图的尺寸。

  • 填充(Padding):为了保持特征图的尺寸,可能在输入的边缘添加零(称为填充)。常见的填充方式有“有效卷积”(没有填充)和“同态卷积”(填充以保持输入和输出尺寸相同)。

    案例1:一个特征图尺寸为4 * 4的输入,使用3 * 3的卷积核,步幅=1,填充=0,输出的尺寸=(4 - 3)/1 + 1 = 2
    在这里插入图片描述
    案例2:一个特征图尺寸为5 * 5的输入,使用3 * 3的卷积核,步幅=1,填充=1,输出的尺寸=(5 + 2 * 1 - 3)/1 + 1 = 5
    在这里插入图片描述
    案例3:一个特征图尺寸为5 * 5的输入, 使用3 * 3的卷积核,步幅=2,填充=0,输出的尺寸=(5-3)/2 + 1 = 2
    在这里插入图片描述
    案例4:一个特征图尺寸为6 * 6的输入, 使用3 * 3的卷积核,步幅=2,填充=1,输出的尺寸=(6 + 2 * 1 - 3)/2 + 1 = 2.5 + 1 = 3.5 向下取整=3(降采样:边长减少1/2)
    在这里插入图片描述

  1. 卷积层的优点
  • 局部连接:卷积层只关注输入数据的局部区域,这使得模型可以有效地捕捉空间特征。
  • 参数共享:同一个卷积核在整个输入上共享参数,大大减少了模型的参数数量,提高了计算效率。
  • 平移不变性:卷积层对输入数据的小幅平移具有鲁棒性,使得模型更具泛化能力。
  1. 卷积层的类型
  • 标准卷积:最常用的卷积层类型。
  • 深度可分卷积(Depthwise Separable Convolution):将卷积分为两步,首先在每个通道上应用卷积,然后将结果结合。这种方法可以减少计算复杂度。
  • 转置卷积(Transposed Convolution):用于上采样,通常在生成模型(如生成对抗网络)中使用。

这里以Conv2d为例,它是 PyTorch 中用于实现二维卷积的类,常用于计算机视觉任务,如图像分类、目标检测等。它可以有效地提取图像的空间特征。

Stride

  • stride: 这是卷积核在输入数据上每次移动的像素数。
    • 如果 stride=1,卷积核每次移动一个像素。
    • 如果 stride=2,卷积核每次移动两个像素,导致输出特征图的尺寸减半。
    • 在标准的二维卷积中,卷积操作的方向是固定的,卷积核在水平和垂直方向上都以指定的步幅移动。如果想在不同方向上使用不同的步幅,可以使用 stride 的元组(例如 (stride_h, stride_w))来分别指定高度和宽度的步幅

在这里插入图片描述
在这里插入图片描述

如图所示,当我们输入图像时,会与卷积核在数据上滑动,滑动时的方向、步长由Stride控制,滑动时会进行点积操作,生成卷积后的输出。

Padding

padding 是在输入特征图的边缘添加额外的像素,以便在进行卷积时保持特征图的大小或者根据特定需求调整特征图的大小。常见的用途包括:

  • 保持特征图尺寸:通过适当的填充,可以使得卷积操作前后特征图的空间维度(宽度和高度)相同。
  • 避免信息丢失:填充可以防止卷积过程中丢失输入图像的边缘信息。
  • 改善模型学习能力:适当的填充可以帮助模型更好地捕捉图像的全局信息。

padding 参数

torch.nn.Conv2d 中,padding 参数可以是一个整数或一个元组。它的具体含义如下:

  • 整数:如果指定为单个整数(例如 padding=1),则在所有四个边(上、下、左、右)添加相同数量的填充。
  • 元组:如果指定为一个元组(例如 padding=(1, 2)),则分别在高度和宽度方向上指定填充的数量。这里的第一个值是高度方向的填充,第二个值是宽度方向的填充。
  • 在这里插入图片描述
    我们通过代码实现一下上面的操作;
import torch
import torch.nn.functional as F

# 定义输入数据,形状为(5, 5),代表一个5x5的二维矩阵
input = torch.tensor([[1,2,0,3,1],
                      [0,1,2,3,1],
                      [1,2,1,0,0],
                      [5,2,3,1,1],
                      [2,1,0,1,1]])

# 定义卷积核,形状为(3, 3),用于对输入数据进行卷积操作
kernel = torch.tensor([[1,2,1],
                       [0,1,0],
                       [2,1,0]])
#一般卷积层的图像输入需要是 四维张量(batch size, channels, height, width)
# 重塑输入数据,使其变为四维张量,满足卷积层输入要求
input = torch.reshape(input, (1,1,5,5))
# 重塑卷积核,使其变为四维张量,满足卷积层权重要求,卷积核张量的形状为(out_channels, in_channels, kernel_height, kernel_width)
kernel = torch.reshape(kernel, (1,1,3,3))

# 打印重塑后的输入数据形状
print(input.shape) # torch.Size([1, 1, 5, 5])
# 打印重塑后的卷积核形状
print(kernel.shape) # torch.Size([1, 1, 3, 3])

# 执行二维卷积操作,stride=1表示卷积步长为1
output = F.conv2d(input, kernel, stride=1)
'''
输出结果:
tensor([[[[10, 12, 12],
          [18, 16, 16],
          [13,  9,  3]]]])
'''
print(output)

# 执行二维卷积操作,stride=2表示卷积步长为2,结果是下采样
output2 = F.conv2d(input, kernel, stride=2)
'''
输出结果:
tensor([[[[10, 12],
          [13,  3]]]])
'''
print(output2)

# 执行二维卷积操作,stride=1,padding=1表示在输入数据周围填充一圈零
output3 = F.conv2d(input, kernel, stride=1, padding=1)
'''
输出结果:
tensor([[[[ 1,  3,  4, 10,  8],
          [ 5, 10, 12, 12,  6],
          [ 7, 18, 16, 16,  8],
          [11, 13,  9,  3,  4],
          [14, 13,  9,  7,  4]]]])
'''
print(output3)

池化

池化(Pooling)是卷积神经网络(CNN)中一个重要的操作,主要用于降低特征图(Feature Map)的空间尺寸,从而减少计算量、减少参数数量并抑制过拟合。池化操作通过对输入特征图的局部区域进行下采样,提取出显著特征,同时保留重要的空间信息。以下是关于池化的详细介绍:

  1. 池化的类型

​ 1.1 最大池化(Max Pooling)

  • 定义:从每个局部区域中提取最大值。

  • 优点:能够保留特征的显著性,常用于处理图像数据。

  • 应用:例如,在2x2的最大池化中,对于一个局部区域,它会选择这个区域中的最大值作为输出。

    最大池化层有时也被称为下采样。dilation为空洞卷积,如下图所示。池化使得数据由5 * 5 变为3 * 3,甚至1 * 1的,这样导致计算的参数会大大减小。例如1080P的电影经过池化的转为720P的电影、或360P的电影后,同样的网速下,视频更为流畅。
    在这里插入图片描述

  • 示例

    输入特征图:
    [[1, 3, 2, 4],
     [5, 6, 1, 0],
     [3, 2, 0, 1],
     [4, 8, 7, 6]]
    
    2x2 最大池化结果:
    [[6, 4],
     [4, 8]]
    

​ 1.2 平均池化(Average Pooling)

  • 定义:从每个局部区域中计算平均值。

  • 优点:可以提供更平滑的特征图,可能在某些情况下比最大池化更稳健。

  • 应用:同样是使用2x2的窗口,计算窗口内所有元素的平均值。

    示例

    输入特征图:
    [[1, 3, 2, 4],
     [5, 6, 1, 0],
     [3, 2, 0, 1],
     [4, 8, 7, 6]]
    
    2x2 平均池化结果:
    [[3.75, 2.25],
     [3.25, 5.25]]
    

​ 1.3 全局池化(Global Pooling)

  • 定义:对整个特征图进行池化,通常用于将特征图缩减为单一值。
  • 应用:全局最大池化和全局平均池化是常见形式,通常用于卷积网络的最后一层,以便降低特征维度。
  1. 池化的作用
  • 降维:通过减少特征图的尺寸,池化减少了计算复杂度,降低了后续层的参数数量。
  • 特征提取:保留了最显著的特征,有助于模型学习到更强的抽象特征。
  • 提高鲁棒性:对输入图像的微小变化(如平移、旋转)具有一定的鲁棒性,尤其是最大池化。
  • 减少过拟合:通过降低特征维度,池化操作有助于减少模型的复杂度,从而降低过拟合的风险。
  1. 池化的参数
  • 池化窗口大小(Kernel Size):定义池化操作的局部区域的大小。例如,2x2或3x3。
  • 步幅(Stride):定义池化窗口在特征图上移动的步长。步幅设置为2时,池化操作将跳过一个元素。
  • 填充(Padding):通常不在池化操作中使用,但在某些情况下,可以通过填充输入特征图来控制输出的尺寸。
  • 膨胀(dilation):定义卷积或池化操作中核的扩张(dilated convolution)。扩张卷积通过在卷积核的元素之间插入空洞(零)来增加感受野,而不增加参数数量。扩张卷积能有效捕获更大范围的特征,同时保持计算效率。
  • 返回索引(return_indices)return_indices 参数通常与 MaxPool2d 一起使用。设置为 True 时,该池化操作会返回最大值的索引。这些索引可以用于反向传播,以便在反向传递梯度时能够准确恢复被池化的特征。当使用 MaxPool2d 并且 return_indices=True 时,可以在后续的 Unpool 操作中使用这些索引,精确地将特征图恢复到池化前的形状。这在某些任务中(如图像分割)是非常有用的,因为它可以保留位置信息。
  • 向上取整(ceil_mode)ceil_mode 参数用于控制在进行池化时,输出特征图的形状如何计算。具体来说,它决定了在计算输出维度时,ceil_mode = True 则表示保留所有区域,若ceil_mode = False 则表示去除不完整的区域(右上、左下、右下都是有空白区域则为不完整区域)

池化的替代方法

在一些现代的神经网络架构中,池化操作可能被替代为其他方法,如:

  • 卷积层:使用步幅大于1的卷积层代替池化,能够同时执行特征提取和下采样。
  • 空洞卷积(Dilated Convolution):通过调整卷积核的间距来增加感受野,减少对池化的需求。

最大池化:以池化核(kernel_size=3)为例,它表示从3×3的区域中取最大值,来替代原有区域的值,此时默认Stride = 3,会在原图像中进行移动

在这里插入图片描述
在这里插入图片描述
我们用代码测试一下,看看是不是我们预期的结果

import torch
import torchvision.datasets
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter


dataset = torchvision.datasets.CIFAR10("../dataset",train= False,download=True,transform= torchvision.transforms.ToTensor)

dataloader = DataLoader(dataset,batch_size=64)

# 定义输入数据,形状为(5, 5),代表一个5x5的二维矩阵
input = torch.tensor([[1,2,0,3,1],
                      [0,1,2,3,1],
                      [1,2,1,0,0],
                      [5,2,3,1,1],
                      [2,1,0,1,1]],dtype = torch.float32)

# 重塑输入数据,使其变为四维张量,满足卷积层输入要求
input = torch.reshape(input, (-1,1,5,5))

class MyMaxPool(nn.Module):
    def __init__(self):
        super(MyMaxPool, self).__init__()
        self.maxPool = nn.MaxPool2d(kernel_size=3, ceil_mode=False)

    def forward(self,input):
        output = self.maxPool(input)
        return output

'''
ceil_mode = True
tensor([[[[2., 3.],
          [5., 1.]]]])
ceil_mode = False
tensor([[[[2.]]]])          
'''

我们接下来用之前的训练集来试一下最大池化:

import torchvision
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter

dataset = torchvision.datasets.CIFAR10("./dataset",train= False,download=True,transform= torchvision.transforms.ToTensor())

dataloader = DataLoader(dataset,batch_size=64)

class MyMaxPool(nn.Module):
    def __init__(self):
        super(MyMaxPool, self).__init__()
        self.maxPool = nn.MaxPool2d(kernel_size=3, ceil_mode=False)

    def forward(self,input):
        output = self.maxPool(input)
        return output

myMaxPool = MyMaxPool()
writer = SummaryWriter("logs_maxpool")
step = 0
for datas in dataloader:
    imgs, targets = datas
    writer.add_images("input", imgs, step)
    output = myMaxPool(imgs)
    writer.add_images("output", output,step)
    step += 1

writer.close()

在这里插入图片描述
在这里插入图片描述

非线性激活

非线性激活函数是神经网络中的一种关键组成部分,主要用于引入非线性特征,以便网络能够学习和逼近复杂的函数。非线性激活函数是指在神经元输出上应用的函数,使得输出与输入之间的关系不再是线性的。通过引入非线性,神经网络可以组合多个线性变换,从而能够捕捉更复杂的数据模式。

常见的非线性激活函数

以下是一些常用的非线性激活函数:

  • ReLU(Rectified Linear Unit)
    • 公式: ( f(x) = \max(0, x) )
    • 特点: 简单且计算效率高,能够有效缓解梯度消失问题,但可能导致“神经元死亡”现象。
  • Sigmoid
    • 公式: ( f(x) = \frac{1}{1 + e^{-x}} )
    • 特点: 输出范围在 (0, 1) 之间,常用于二分类任务,但在深层网络中容易导致梯度消失。
  • Tanh(双曲正切函数)
    • 公式: ( f(x) = \tanh(x) = \frac{e^x - e{-x}}{ex + e^{-x}} )
    • 特点: 输出范围在 (-1, 1) 之间,相较于 Sigmoid,Tanh 的输出均值为零,有助于加快收敛。
  • Leaky ReLU
    • 公式: ( f(x) = \max(0.01x, x) )
    • 特点: 解决了 ReLU 的神经元死亡问题,通过在负区间引入小的斜率。
  • Softmax
    • 公式: ( f(x_i) = \frac{e^{x_i}}{\sum_{j} e^{x_j}} )
    • 特点: 将输入转换为概率分布,通常用于多分类任务的输出层。

这里以ReLu为例,ReLu有一个参数是inplace,它默认为False,如果为True的话就表示激活函数的计算将直接在输入上进行,不会保留输入的原始数据,Relu的计算规则如下:

  • 当输入 ( x ) 大于0时,输出等于输入本身(( f(x) = x ))。
  • 当输入 ( x ) 小于或等于0时,输出为0(( f(x) = 0 ))。
import  torch
from torch import nn

input = torch.tensor([[1,-0.5],
                      [-1,3]])

input = torch.reshape(input,(-1,1,2,2))
print(input.shape) # torch.Size([1, 1, 2, 2])

class MyReLU(nn.Module):
    def __init__(self):
        super(MyReLU, self).__init__()
        self.relu = nn.ReLU()
    def forward(self,input):
        output = self.relu(input)
        return output


myRelu = MyReLU()
output = myRelu(input)
'''
tensor([[1., 0.],
        [0., 3.]])
'''
print(output)

Relu对图像处理的效果并不明显,使用Sigmoid对图像进行处理,再对比一下

只需要调整一下方法内调用的激活函数即可

def __init__(self):
    super(MyReLU, self).__init__()
    self.relu = ReLU()
    self.sigmoid = Sigmoid()
def forward(self,input):
    output = self.sigmoid(input)
    return output

在这里插入图片描述

Sequential

PyTorch中,Sequential是一个非常实用且简单的模块,用于顺序堆叠神经网络层。它允许你通过定义一个包含多个层的列表来构建模型,而不需要显式地定义前向传播函数。

在PyTorch里,torch.nn.Sequential可以被视为一个容器,将各个子模块按照它们添加到Sequential对象中的顺序进行连接,并以此顺序应用这些层。这对于创建由简单的线性堆叠(例如全连接层、卷积层)组成的网络非常有用,避免了手动编写前向传播函数的麻烦。

构建一个简单神经网络的案例:

import torch
from torch import nn

# 定义一个具有两个隐藏层和一个输出层的简单的全连接神经网络
model = nn.Sequential(
    nn.Linear(10, 20),   # 输入特征数为10,输出特征数为20的第一个全连接层
    nn.ReLU(),           # 激活函数ReLU
    nn.Linear(20, 15),   # 第二个全连接层,输入特征数为20(前一层的输出),输出特征数为15
    nn.ReLU(),
    nn.Linear(15, 5)     # 输出层,最后的全连接层将15个输入映射到5个输出
)

# 现在你可以像对待其他PyTorch模型一样使用这个model对象,
# 比如传递张量通过它以获得预测结果:
input_data = torch.randn(1, 10)   # 假设批大小为1,输入特征数为10
output = model(input_data)
print(output)

在上面的例子中,Sequential容器将三个Linear层和两个ReLU激活函数按照定义的顺序连接起来。数据会依次通过这些模块。

需要注意的是,如果模型需要更加复杂的结构,例如循环网络、跳过连接或残差块等,使用普通的类并显式地实现前向传播方法(如在__init__中自定义层,并在forward方法中编写逻辑)可能更合适。但对于直线型堆叠的网络架构,Sequential提供了一种非常快速和直观的方式来构建模型。

构建神经网络

这里以CIFAR-10模型结构为例,图片中从 flatten展平的时候省略了线性操作,从 64×4×4 到 64 需要线性转换

在这里插入图片描述

import torch
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
from torch.utils.tensorboard import SummaryWriter


class MyNN(nn.Module):
    """
    自定义神经网络模型类,继承自nn.Module。
    该模型包括卷积层、最大池化层、全连接层,用于图像分类任务。
    """
    def __init__(self):
        """
        构造函数,初始化神经网络的层结构。
        """
        super(MyNN, self).__init__()
        # 定义模型结构,使用Sequential容器将各层包装起来
        self.modelS = Sequential(
            Conv2d(3,32,5,padding=2),  # 第一个卷积层,输入通道数为3,输出通道数为32,卷积核大小为5x5,padding为2
            MaxPool2d(2),  # 第一个最大池化层,池化窗口大小为2x2
            Conv2d(32, 32, 5, padding=2),  # 第二个卷积层,输入通道数为32,输出通道数为32,卷积核大小为5x5,padding为2
            MaxPool2d(2),  # 第二个最大池化层,池化窗口大小为2x2
            Conv2d(32, 64, 5, padding=2),  # 第三个卷积层,输入通道数为32,输出通道数为64,卷积核大小为5x5,padding为2
            MaxPool2d(2),  # 第三个最大池化层,池化窗口大小为2x2
            Flatten(),  # 扁平化层,将多维特征图展平为一维向量
            Linear(1024, 64),  # 第一个全连接层,输入特征数为1024,输出特征数为64
            Linear(64, 10)  # 第二个全连接层,输入特征数为64,输出特征数为10
        )
    
    def forward(self, x):
        """
        前向传播函数,定义数据通过模型时的处理流程。
        
        参数:
        x (Tensor): 输入数据,通常为一个批次的图像数据。
        """
        x = self.modelS(x)  # 将输入数据通过Sequential容器定义的所有层进行前向传播
        return x  # 返回前向传播的结果

myNn = MyNN()
print(myNn)

input = torch.ones((64,3,32,32))
output = myNn(input)
print(output)

writer = SummaryWriter("NN")
writer.add_graph(myNn, input)
writer.close()

在这里插入图片描述

损失函数

损失函数(Loss Function)

损失函数是用来衡量模型预测与实际结果之间差距的函数。在训练模型时,我们希望通过最小化损失函数来提高模型的性能。不同的任务通常会使用不同的损失函数,以下是一些常见的损失函数:

  • 均方误差(Mean Squared Error, MSE)
    • 主要用于回归问题。它计算预测值与真实值之间的差的平方的平均值。公式如下: [ \text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 ]
    • 其中 (y_i) 是真实值,(\hat{y}_i) 是预测值。
  • 交叉熵损失(Cross-Entropy Loss)
    • 常用于分类问题,特别是多类分类。它量化了真实分布与预测分布之间的差异。公式如下: [ \text{Cross-Entropy} = -\sum_{i=1}^{C} y_i \log(\hat{y}_i) ]
    • 其中 © 是类别数,(y_i) 是真实类别的指示变量(1或0),(\hat{y}_i) 是预测概率。
  • 对比损失(Contrastive Loss)
    • 主要用于度量学习,尤其是在处理成对样本时。

这里以交叉熵损失函数为例

在这里插入图片描述

import torch
from torch import nn

# 初始化一个张量x,包含三个浮点数,代表神经网络的输出
x = torch.tensor([0.1,0.2,0.3])
# 初始化一个张量y,包含一个整数,代表目标标签
y = torch.tensor([1])
# 将x重塑为形状为(1,3)的张量,以适配CrossEntropyLoss的输入要求
x = torch.reshape(x,(1,3))
# 实例化一个CrossEntropyLoss对象,用于计算损失
loss_cross = nn.CrossEntropyLoss()
# 使用CrossEntropyLoss计算x和y之间的损失
result_cross = loss_cross(x,y)


print(result_cross) # tensor(1.1019)

−0.2+ln(exp(0.1)+exp(0.2)+exp(0.3))=1.1019-0.2+ln(exp(0.1)+exp(0.2)+exp(0.3)) = 1.10190.2+ln(exp(0.1)+exp(0.2)+exp(0.3))=1.1019

反向传播

反向传播(Backpropagation)

反向传播是一种计算神经网络中权重梯度的有效方法,它利用链式法则来计算损失函数相对于每个参数的导数。通过反向传播算法,模型可以通过梯度下降等优化算法调整权重,以最小化损失函数。

反向传播的基本步骤:

  1. 前向传播
    • 输入数据通过神经网络进行前向传播,计算出预测结果和损失。
  2. 计算损失
    • 使用损失函数计算出预测值与真实值之间的损失。
  3. 反向传播
    • 从输出层开始,逐层计算损失函数对每个参数的梯度。
    • 使用链式法则:如果有函数 (z = f(y) = f(g(x))),那么: [ \frac{dz}{dx} = \frac{dz}{dy} \cdot \frac{dy}{dg} \cdot \frac{dg}{dx} ],链式法则(Chain Rule),是微积分中的一个重要概念,用于计算复合函数的导数。它的含义是,如果一个变量 ( z ) 是通过多个中间变量(比如 ( y ) 和 ( g ))来定义的,那么 ( z ) 对 ( x ) 的导数可以通过这些中间变量的导数来计算。
    • 在神经网络中,这意味着我们需要从输出层向输入层反向传播梯度。
  4. 更新参数
    • 使用计算得到的梯度更新网络的权重。例如,使用简单的梯度下降法: [ w = w - \eta \frac{\partial L}{\partial w} ]
    • 其中 (w) 是权重,(\eta) 是学习率,(\frac{\partial L}{\partial w}) 是损失函数对权重的梯度。

这里以我们之前构建的神经网络为例

import torchvision.datasets
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
from torch.utils.data import DataLoader

dataset = torchvision.datasets.CIFAR10("./dataset",train= False,transform=torchvision.transforms.ToTensor(),download=True)
dataloader = DataLoader(dataset,batch_size=1)
class MyNN(nn.Module):
    """
    自定义神经网络模型类,继承自nn.Module。
    该模型包括卷积层、最大池化层、全连接层,用于图像分类任务。
    """
    def __init__(self):
        """
        构造函数,初始化神经网络的层结构。
        """
        super(MyNN, self).__init__()
        # 定义模型结构,使用Sequential容器将各层包装起来
        self.modelS = Sequential(
            Conv2d(3,32,5,padding=2),  # 第一个卷积层,输入通道数为3,输出通道数为32,卷积核大小为5x5,padding为2
            MaxPool2d(2),  # 第一个最大池化层,池化窗口大小为2x2
            Conv2d(32, 32, 5, padding=2),  # 第二个卷积层,输入通道数为32,输出通道数为32,卷积核大小为5x5,padding为2
            MaxPool2d(2),  # 第二个最大池化层,池化窗口大小为2x2
            Conv2d(32, 64, 5, padding=2),  # 第三个卷积层,输入通道数为32,输出通道数为64,卷积核大小为5x5,padding为2
            MaxPool2d(2),  # 第三个最大池化层,池化窗口大小为2x2
            Flatten(),  # 扁平化层,将多维特征图展平为一维向量
            Linear(1024, 64),  # 第一个全连接层,输入特征数为1024,输出特征数为64
            Linear(64, 10)  # 第二个全连接层,输入特征数为64,输出特征数为10
        )

    def forward(self, x):
        """
        前向传播函数,定义数据通过模型时的处理流程。

        参数:
        x (Tensor): 输入数据,通常为一个批次的图像数据。
        """
        x = self.modelS(x)  # 将输入数据通过Sequential容器定义的所有层进行前向传播
        return x  # 返回前向传播的结果

myNn = MyNN()
loss = nn.CrossEntropyLoss()

# 遍历数据加载器提供的每一批数据
for data in dataloader:
    # 将一批数据分割为图像和目标值
    imgs, targets = data
    # 将图像数据通过自定义的神经网络进行前向传播,获取网络输出
    outputs = myNn(imgs)
    # 打印神经网络的输出结果
    print(outputs)
    # 打印实际的目标值,用于对比和调试
    print(targets)

    '''
    tensor([[-0.0223,  0.0126, -0.0648,  0.0947,  0.0119,  0.0283, -0.0693, -0.0278,
          0.0739,  0.0202]], grad_fn=<AddmmBackward0>)
    tensor([9])
    '''

其中outputs 指的是该图片属于某个类别的概率,targets 指的是实际的目标值,在CIFAR-10模型中,outputstargets 之间的关系是模型预测和实际标签之间的关系。具体来说:

  1. Outputs:这是模型对每个输入图像的预测结果,通常以概率形式表示。对于CIFAR-10,模型会输出一个长度为10的向量,每个元素对应于一个类(CIFAR-10包含10个类,如飞机、汽车、鸟等)。这些输出通常是经过Softmax函数处理的,表示每个类的预测概率。
  2. Targets:这是实际的标签,表示每个输入图像的真实类别。在CIFAR-10中,目标标签也是一个长度为10的向量,通常用one-hot编码表示。例如,如果一个图像的真实标签是“汽车”,那么对应的目标向量可能是 [0, 1, 0, 0, 0, 0, 0, 0, 0, 0]

分类正确的标准:如果对于一个给定的输入图像,模型输出的类别概率中最大值所对应的索引(即模型的预测类别)与目标标签中为1的位置(即真实类别)相同,则认为分类正确。

  • 例如:
    • 如果 outputs = [0.1, 0.8, 0.05, 0.05, 0, 0, 0, 0, 0, 0](表示模型预测的概率)
    • 对应的 targets = [0, 1, 0, 0, 0, 0, 0, 0, 0, 0](表示实际类别是“汽车”)

在这种情况下,模型的预测类别是索引1(概率最大),而真实类别也是1,因此分类正确。

我们接下来调整一下代码,输出一下损失函数,看看实际输出和目标之间的差距,这能为我们更新输出提供一定依据

# 初始化交叉熵损失函数
loss = CrossEntropyLoss()
# 遍历数据加载器提供的每一批数据
for data in dataloader:
    # 将一批数据分割为图像和目标值
    imgs, targets = data
    # 将图像数据通过自定义的神经网络进行前向传播,获取网络输出
    outputs = myNn(imgs)

    # 计算模型输出结果的损失
    result_loss = loss(outputs, targets)
    print(result_loss)

输出如下:

tensor(2.3745, grad_fn=<NllLossBackward0>)
tensor(2.2670, grad_fn=<NllLossBackward0>)
tensor(2.2648, grad_fn=<NllLossBackward0>)
tensor(2.2704, grad_fn=<NllLossBackward0>)
tensor(2.3340, grad_fn=<NllLossBackward0>)
tensor(2.3180, grad_fn=<NllLossBackward0>)
......

这时候可以使用输出梯度

# 计算模型输出结果的损失
result_loss = loss(outputs, targets)
# 对损失进行反向传播,以计算梯度
result_loss.backward()

我们调试代码,将断点打在计算梯度的代码上,我们先不执行这行代码,查看modules 下的神经网络,这里以我们的卷积层为例

首先在调试平台找到我们的模型,打开模型下的modelS 找到被保护的属性,打开modules 就可以看到我们的各层,第一层就是我们设置的卷积层

在这里插入图片描述
在这里插入图片描述
找到卷积层下的weight,可以看到现在的grad 是None,
在这里插入图片描述
现在我们执行计算梯度的代码,结果如下图所示,可以看到生成了梯度
在这里插入图片描述
一旦计算出了梯度,通常会使用优化器(如 SGD、Adam 等)来更新模型的参数,以减少损失

优化器

定义

  • 优化器 是一种算法,用于在训练过程中自动调整模型的参数(如权重和偏置),以最小化损失函数。
  • 损失函数衡量了模型预测值与实际值之间的差距。通过不断减小这个差距,模型的性能会逐渐提高。

常见的优化器

  1. 随机梯度下降 (SGD)
    • 原理 :每次更新使用一个样本或一小批样本的梯度来调整参数。
    • 特点 :简单、计算效率高,但可能不稳定,需要手动调学习率。
  2. 动量 (Momentum)
    • 原理 :在 SGD 的基础上引入了一个累积历史梯度的动量项,帮助加速收敛并减少振荡。
    • 特点 :更快的收敛速度,适用于非凸优化问题。
  3. Nesterov 加速梯度 (NAG)
    • 原理 :对 Momentum 进行改进,在计算梯度时考虑未来的位置,进一步减少振荡。
    • 特点 :更准确的梯度估计,更快的收敛速度。
  4. AdaGrad
    • 原理 :为每个参数分配独立的学习率,适应不同特征尺度的问题。
    • 特点 :处理稀疏数据效果好,但学习率会逐渐变小,可能导致训练过早停止。
  5. RMSProp
    • 原理 :通过引入衰减项来平滑学习率的变化,解决 AdaGrad 的问题。
    • 特点 :对非凸优化问题有较好的性能,避免学习率过快下降。
  6. Adam (Adaptive Moment Estimation)
    • 原理 :结合了动量和 RMSProp 的优点,是一种非常流行的自适应学习率优化算法。
    • 特点 :收敛速度快,适用于各种类型的优化问题,特别是大规模深度学习任务。
  7. AdamW
    • 原理 :对 Adam 进行改进,通过在权重衰减项上进行修正,提高稳定性。
    • 特点 :改善了 Adam 在某些情况下可能导致训练不稳定的缺点。

我们这里用优化器进行一次优化

import torch
import torchvision.datasets
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential, CrossEntropyLoss
from torch.utils.data import DataLoader

dataset = torchvision.datasets.CIFAR10("./dataset",train= False,transform=torchvision.transforms.ToTensor(),download=True)
dataloader = DataLoader(dataset,batch_size=64,drop_last= True)
class MyNN(nn.Module):
    """
    自定义神经网络模型类,继承自nn.Module。
    该模型包括卷积层、最大池化层、全连接层,用于图像分类任务。
    """
    def __init__(self):
        """
        构造函数,初始化神经网络的层结构。
        """
        super(MyNN, self).__init__()
        # 定义模型结构,使用Sequential容器将各层包装起来
        self.modelS = Sequential(
            Conv2d(3,32,5,padding=2),  # 第一个卷积层,输入通道数为3,输出通道数为32,卷积核大小为5x5,padding为2
            MaxPool2d(2),  # 第一个最大池化层,池化窗口大小为2x2
            Conv2d(32, 32, 5, padding=2),  # 第二个卷积层,输入通道数为32,输出通道数为32,卷积核大小为5x5,padding为2
            MaxPool2d(2),  # 第二个最大池化层,池化窗口大小为2x2
            Conv2d(32, 64, 5, padding=2),  # 第三个卷积层,输入通道数为32,输出通道数为64,卷积核大小为5x5,padding为2
            MaxPool2d(2),  # 第三个最大池化层,池化窗口大小为2x2
            Flatten(),  # 扁平化层,将多维特征图展平为一维向量
            Linear(1024, 64),  # 第一个全连接层,输入特征数为1024,输出特征数为64
            Linear(64, 10)  # 第二个全连接层,输入特征数为64,输出特征数为10
        )

    def forward(self, x):
        """
        前向传播函数,定义数据通过模型时的处理流程。

        参数:
        x (Tensor): 输入数据,通常为一个批次的图像数据。
        """
        x = self.modelS(x)  # 将输入数据通过Sequential容器定义的所有层进行前向传播
        return x  # 返回前向传播的结果

myNn = MyNN()
# 初始化交叉熵损失函数
loss = CrossEntropyLoss()

optim = torch.optim.SGD(myNn.parameters(),lr=0.01)

# 遍历数据加载器提供的数据批次
for data in dataloader:
    # 将数据批次解包为图像和目标值
    imgs, targets = data
    # 使用神经网络模型对图像进行处理,得到模型输出
    outputs = myNn(imgs)
    # 计算模型输出与实际目标值之间的损失
    result_loss = loss(outputs, targets)
    # 在进行反向传播之前,将优化器的梯度缓冲区清零
    optim.zero_grad()
    # 对损失进行反向传播,计算模型参数的梯度
    result_loss.backward()
    # 使用优化器对模型参数进行一步更新
    optim.step()

我们在优化器对模型参数更新前断点查看modules下的卷积层的参数变化
在这里插入图片描述
我们多执行几次看看权重的变化
在这里插入图片描述
可以看到参数在不断的调整,我们打印一下优化一轮后的损失函数,我们会发现并没有特别明显的变化,这是因为一般会进行多轮优化。

Files already downloaded and verified
tensor(2.3054, grad_fn=<NllLossBackward0>)
tensor(2.3163, grad_fn=<NllLossBackward0>)
tensor(2.3126, grad_fn=<NllLossBackward0>)
tensor(2.2961, grad_fn=<NllLossBackward0>)
tensor(2.3248, grad_fn=<NllLossBackward0>)
tensor(2.3139, grad_fn=<NllLossBackward0>)
tensor(2.3148, grad_fn=<NllLossBackward0>)
tensor(2.2953, grad_fn=<NllLossBackward0>)
......
tensor(2.2901, grad_fn=<NllLossBackward0>)
tensor(2.2784, grad_fn=<NllLossBackward0>)
tensor(2.2907, grad_fn=<NllLossBackward0>)
tensor(2.2730, grad_fn=<NllLossBackward0>)
tensor(2.2824, grad_fn=<NllLossBackward0>)
tensor(2.2758, grad_fn=<NllLossBackward0>)

我们调整代码,进行多轮优化,在代码中,我们采取比较每一轮累计损失的方式去判断优化的效果

# 开始训练循环,设定训练的轮数为20轮
for epoch in range(20):
    # 初始化每轮训练的累计损失为0
    running_loss = 0.0
    # 遍历数据加载器提供的数据批次
    for data in dataloader:
        # 将数据批次解包为图像和目标值
        imgs, targets = data
        # 使用神经网络模型对图像进行处理,得到模型输出
        outputs = myNn(imgs)
        # 计算模型输出与实际目标值之间的损失
        result_loss = loss(outputs, targets)
        # 在进行反向传播之前,将优化器的梯度缓冲区清零
        optim.zero_grad()
        # 对损失进行反向传播,计算模型参数的梯度
        result_loss.backward()
        # 使用优化器对模型参数进行一步更新
        optim.step()
        # 累加当前批次的损失到累计损失中
        running_loss = running_loss + result_loss
    print(running_loss)
tensor(358.7988, grad_fn=<AddBackward0>)
tensor(356.2275, grad_fn=<AddBackward0>)
tensor(349.1218, grad_fn=<AddBackward0>)
tensor(329.5598, grad_fn=<AddBackward0>)
tensor(311.7713, grad_fn=<AddBackward0>)
tensor(301.0726, grad_fn=<AddBackward0>)
tensor(292.2879, grad_fn=<AddBackward0>)
tensor(285.0728, grad_fn=<AddBackward0>)
tensor(277.6305, grad_fn=<AddBackward0>)
tensor(270.5865, grad_fn=<AddBackward0>)
tensor(264.1595, grad_fn=<AddBackward0>)
tensor(258.3930, grad_fn=<AddBackward0>)
tensor(253.2159, grad_fn=<AddBackward0>)
tensor(248.5789, grad_fn=<AddBackward0>)
tensor(244.3835, grad_fn=<AddBackward0>)
tensor(240.5016, grad_fn=<AddBackward0>)
tensor(236.8815, grad_fn=<AddBackward0>)
tensor(233.4750, grad_fn=<AddBackward0>)
tensor(230.2215, grad_fn=<AddBackward0>)
tensor(227.0506, grad_fn=<AddBackward0>)

可以看到优化器对我们的模型参数不断优化,损失在不断的减少

除了直接使用优化器,我们还可以使用学习率调度器,这里以StrpLR为例,它在深度学习中被广泛用于优化训练过程。当与随机梯度下降(Stochastic Gradient Descent, SGD)等优化算法结合使用时,StepLR 可以显著提高模型的性能和收敛速度。

StepLR 的作用

  1. 动态调整学习率 :在训练初期,较大的学习率可以帮助模型快速接近损失函数的最小值区域。然而,过高的学习率可能导致模型跳过最优解或导致训练不稳定。随着训练的进行,逐步减小学习率可以使得优化过程更加精细,有助于找到更精确的最优点。
  2. 防止过拟合 :在训练后期,较低的学习率可以减少对噪声数据的过度拟合,从而提高模型的泛化能力。
  3. 加速收敛 :通过合理设置学习率的变化规律,StepLR 可以帮助模型更快地收敛到最优解或接近最优解的状态。

StepLR 的工作原理

StepLR 通过在预定的时间点(通常是训练过程中的某些 epoch)按固定的比例减少学习率。具体来说,它使用以下公式来调整学习率:

new_lr=base_lr×γ⌊step_sizeepoch⌋

其中:

  • base_lr 是初始学习率。
  • \(\gamma\) 是学习率衰减系数,通常是一个小于1的正数(例如0.1)。
  • step_size 是每隔多少个 epoch 减少一次学习率。

为什么需要使用StepLR:

  1. 适应不同的训练阶段 :在训练的不同阶段,模型对学习率的需求是不同的。初期需要较大的学习率来快速收敛,后期需要较小的学习率来精细调整权重。
  2. 提高模型性能 :通过动态调整学习率,可以更好地平衡训练的稳定性和速度,从而提高最终模型的性能。
  3. 避免局部最优解 :合理的学习率调度可以帮助模型跳出局部最优解,找到全局最优或接近全局最优的解决方案。

我们用之前的代码试一下,一般来说,StepLR 是放在每个 epoch 结束时进行更新的。这是因为学习率通常需要在一个完整的 epoch 训练完成后才进行调整,这样可以确保模型在每个新的 epoch 开始时使用一个新的、更合适的学习率。:

optim = torch.optim.SGD(myNn.parameters(),lr=0.01)
# 初始化学习率调度器,采用StepLR策略
# StepLR是一种简单且常用的学习率衰减方法,每隔一段时间(由step_size指定)将学习率乘以一个固定的比例(由gamma指定)
# 参数:
#   optim: 优化器,包含要调度学习率的参数
#   step_size: 学习率更新的间隔期,每过step_size个epoch,学习率就会被乘以gamma
#   gamma: 学习率衰减因子,属于(0,1],通常小于1,用于按比例降低学习率
scheduler = torch.optim.lr_scheduler.StepLR(optim, step_size=5, gamma=0.1)

# 开始训练循环,设定训练的轮数为20轮
for epoch in range(20):
    # 初始化每轮训练的累计损失为0
    running_loss = 0.0
    # 遍历数据加载器提供的数据批次
    for data in dataloader:
        # 将数据批次解包为图像和目标值
        imgs, targets = data
        # 使用神经网络模型对图像进行处理,得到模型输出
        outputs = myNn(imgs)
        # 计算模型输出与实际目标值之间的损失
        result_loss = loss(outputs, targets)
        # 在进行反向传播之前,将优化器的梯度缓冲区清零
        optim.zero_grad()
        # 对损失进行反向传播,计算模型参数的梯度
        result_loss.backward()
        # 使用优化器对模型参数进行一步更新
        optim.step()
        # 累加当前批次的损失到累计损失中
        running_loss = running_loss + result_loss
    scheduler.step()  # 
    print(running_loss)

学习率并不会直接帮助我们减少损失,通过动态调整学习率,帮助模型在训练过程中更稳定地收敛到一个更好的解,但通过优化训练过程,间接地有助于减少损失。

tensor(358.6791, grad_fn=<AddBackward0>)
tensor(354.8129, grad_fn=<AddBackward0>)
tensor(343.8545, grad_fn=<AddBackward0>)
tensor(331.1110, grad_fn=<AddBackward0>)
tensor(315.0167, grad_fn=<AddBackward0>)
tensor(299.6557, grad_fn=<AddBackward0>)
tensor(297.4096, grad_fn=<AddBackward0>)
tensor(295.7077, grad_fn=<AddBackward0>)
tensor(294.2353, grad_fn=<AddBackward0>)
tensor(292.8973, grad_fn=<AddBackward0>)
tensor(291.8472, grad_fn=<AddBackward0>)
tensor(291.7124, grad_fn=<AddBackward0>)
tensor(291.5837, grad_fn=<AddBackward0>)
tensor(291.4563, grad_fn=<AddBackward0>)
tensor(291.3298, grad_fn=<AddBackward0>)
tensor(291.2121, grad_fn=<AddBackward0>)
tensor(291.1989, grad_fn=<AddBackward0>)
tensor(291.1861, grad_fn=<AddBackward0>)
tensor(291.1735, grad_fn=<AddBackward0>)
tensor(291.1608, grad_fn=<AddBackward0>)
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐